cli.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. #!/usr/bin/env node
  2. /* es-lint disable */
  3. 'use strict'
  4. // Node requires
  5. const path = require('path')
  6. // npm requires
  7. const meow = require('meow')
  8. const chalk = require('chalk')
  9. const figlet = require('figlet')
  10. const _ = require('lodash')
  11. const { sleep } = require('@joystream/storage-utils/sleep')
  12. const debug = require('debug')('joystream:colossus')
  13. // Project root
  14. const PROJECT_ROOT = path.resolve(__dirname, '..')
  15. // Parse CLI
  16. const FLAG_DEFINITIONS = {
  17. port: {
  18. type: 'number',
  19. alias: 'p',
  20. default: 3000,
  21. },
  22. keyFile: {
  23. type: 'string',
  24. isRequired: (flags, input) => {
  25. // Only required if running server command and not in dev or anonymous mode
  26. if (flags.anonymous || flags.dev) {
  27. return false
  28. }
  29. return input[0] === 'server'
  30. },
  31. },
  32. publicUrl: {
  33. type: 'string',
  34. alias: 'u',
  35. isRequired: (flags, input) => {
  36. // Only required if running server command and not in dev or anonymous mode
  37. if (flags.anonymous || flags.dev) {
  38. return false
  39. }
  40. return input[0] === 'server'
  41. },
  42. },
  43. passphrase: {
  44. type: 'string',
  45. },
  46. wsProvider: {
  47. type: 'string',
  48. default: 'ws://localhost:9944',
  49. },
  50. providerId: {
  51. type: 'number',
  52. alias: 'i',
  53. isRequired: (flags, input) => {
  54. // Only required if running server command and not in dev or anonymous mode
  55. if (flags.anonymous || flags.dev) {
  56. return false
  57. }
  58. return input[0] === 'server'
  59. },
  60. },
  61. ipfsHost: {
  62. type: 'string',
  63. default: 'localhost',
  64. },
  65. anonymous: {
  66. type: 'boolean',
  67. default: false,
  68. },
  69. maxSync: {
  70. type: 'number',
  71. default: 200,
  72. },
  73. }
  74. const cli = meow(
  75. `
  76. Usage:
  77. $ colossus [command] [arguments]
  78. Commands:
  79. server Runs a production server instance
  80. Arguments (required for with server command, unless --dev or --anonymous args are used):
  81. --provider-id ID, -i ID StorageProviderId assigned to you in working group.
  82. --key-file FILE JSON key export file to use as the storage provider (role account).
  83. --public-url=URL, -u URL API Public URL to announce.
  84. Arguments (optional):
  85. --dev Runs server with developer settings.
  86. --passphrase Optional passphrase to use to decrypt the key-file.
  87. --port=PORT, -p PORT Port number to listen on, defaults to 3000.
  88. --ws-provider WS_URL Joystream-node websocket provider, defaults to ws://localhost:9944
  89. --ipfs-host hostname ipfs host to use, default to 'localhost'. Default port 5001 is always used
  90. --anonymous Runs server in anonymous mode. Replicates content without need to register
  91. on-chain, and can serve content. Cannot be used to upload content.
  92. --maxSync The max number of items to sync concurrently. Defaults to 30.
  93. `,
  94. { flags: FLAG_DEFINITIONS }
  95. )
  96. // All-important banner!
  97. function banner() {
  98. console.log(chalk.blue(figlet.textSync('joystream', 'Speed')))
  99. }
  100. function startExpressApp(app, port) {
  101. const http = require('http')
  102. const server = http.createServer(app)
  103. return new Promise((resolve, reject) => {
  104. server.on('error', reject)
  105. server.on('close', (...args) => {
  106. console.log('Server closed, shutting down...')
  107. resolve(...args)
  108. })
  109. server.on('listening', () => {
  110. console.log('API server started.', server.address())
  111. })
  112. server.listen(port, '::')
  113. console.log('Starting API server...')
  114. })
  115. }
  116. // Start app
  117. function startAllServices({ store, api, port, ipfsHttpGatewayUrl, anonymous }) {
  118. const app = require('../lib/app')(PROJECT_ROOT, store, api, ipfsHttpGatewayUrl, anonymous)
  119. return startExpressApp(app, port)
  120. }
  121. // Get an initialized storage instance
  122. function getStorage(runtimeApi, { ipfsHost }) {
  123. // TODO at some point, we can figure out what backend-specific connection
  124. // options make sense. For now, just don't use any configuration.
  125. const { Storage } = require('@joystream/storage-node-backend')
  126. const options = {
  127. resolve_content_id: async (contentId) => {
  128. // Resolve accepted content from cache
  129. const hash = runtimeApi.assets.resolveContentIdToIpfsHash(contentId)
  130. if (hash) return hash
  131. // Resolve via API
  132. const obj = await runtimeApi.assets.getDataObject(contentId)
  133. if (!obj) {
  134. return
  135. }
  136. // if obj.liaison_judgement !== Accepted .. throw ?
  137. return obj.ipfs_content_id.toString()
  138. },
  139. ipfsHost,
  140. }
  141. return Storage.create(options)
  142. }
  143. async function initApiProduction({ wsProvider, providerId, keyFile, passphrase, anonymous }) {
  144. // Load key information
  145. const { RuntimeApi } = require('@joystream/storage-runtime-api')
  146. const api = await RuntimeApi.create({
  147. account_file: keyFile,
  148. passphrase,
  149. provider_url: wsProvider,
  150. storageProviderId: providerId,
  151. })
  152. if (!anonymous && !api.identities.key) {
  153. throw new Error('Failed to unlock storage provider account')
  154. }
  155. await api.untilChainIsSynced()
  156. // We allow the node to startup without correct provider id and account, but syncing and
  157. // publishing of identity will be skipped.
  158. if (!anonymous && !(await api.providerIsActiveWorker())) {
  159. debug('storage provider role account and storageProviderId are not associated with a worker')
  160. }
  161. return api
  162. }
  163. async function initApiDevelopment({ wsProvider }) {
  164. // Load key information
  165. const { RuntimeApi } = require('@joystream/storage-runtime-api')
  166. const api = await RuntimeApi.create({
  167. provider_url: wsProvider,
  168. })
  169. const dev = require('../../cli/dist/commands/dev')
  170. api.identities.useKeyPair(dev.roleKeyPair(api))
  171. // Wait until dev provider is added to role
  172. while (true) {
  173. try {
  174. api.storageProviderId = await dev.check(api)
  175. break
  176. } catch (err) {
  177. debug(err)
  178. }
  179. await sleep(10000)
  180. }
  181. return api
  182. }
  183. // TODO: instead of recursion use while/async-await and use promise/setTimout based sleep
  184. // or cleaner code with generators?
  185. async function announcePublicUrl(api, publicUrl) {
  186. // re-announce in future
  187. const reannounce = function (timeoutMs) {
  188. setTimeout(announcePublicUrl, timeoutMs, api, publicUrl)
  189. }
  190. const chainIsSyncing = await api.chainIsSyncing()
  191. if (chainIsSyncing) {
  192. debug('Chain is syncing. Postponing announcing public url.')
  193. return reannounce(10 * 60 * 1000)
  194. }
  195. // postpone if provider not active
  196. if (!(await api.providerIsActiveWorker())) {
  197. debug('storage provider role account and storageProviderId are not associated with a worker')
  198. return reannounce(10 * 60 * 1000)
  199. }
  200. const sufficientBalance = await api.providerHasMinimumBalance(1)
  201. if (!sufficientBalance) {
  202. debug('Provider role account does not have sufficient balance. Postponing announcing public url.')
  203. return reannounce(10 * 60 * 1000)
  204. }
  205. debug('announcing public url')
  206. try {
  207. await api.workers.setWorkerStorageValue(publicUrl)
  208. debug('announcing complete.')
  209. } catch (err) {
  210. debug(`announcing public url failed: ${err.stack}`)
  211. // On failure retry sooner
  212. debug(`announcing failed, retrying in: 2 minutes`)
  213. reannounce(120 * 1000)
  214. }
  215. }
  216. // Simple CLI commands
  217. let command = cli.input[0]
  218. if (!command) {
  219. command = 'server'
  220. }
  221. const commands = {
  222. server: async () => {
  223. banner()
  224. let publicUrl, port, api
  225. if (cli.flags.dev) {
  226. const dev = require('../../cli/dist/commands/dev')
  227. api = await initApiDevelopment(cli.flags)
  228. port = dev.developmentPort()
  229. publicUrl = `http://localhost:${port}/`
  230. } else {
  231. api = await initApiProduction(cli.flags)
  232. publicUrl = cli.flags.publicUrl
  233. port = cli.flags.port
  234. }
  235. // Get initlal data objects into cache
  236. while (true) {
  237. try {
  238. debug('Fetching data objects')
  239. await api.assets.fetchDataObjects()
  240. break
  241. } catch (err) {
  242. debug('Failed fetching data objects', err)
  243. await sleep(5000)
  244. }
  245. }
  246. // Regularly update data objects
  247. setInterval(async () => {
  248. try {
  249. debug('Fetching data objects')
  250. await api.assets.fetchDataObjects()
  251. } catch (err) {
  252. debug('Failed updating data objects from chain', err)
  253. }
  254. }, 60000)
  255. // TODO: check valid url, and valid port number
  256. const store = getStorage(api, cli.flags)
  257. const ipfsHost = cli.flags.ipfsHost
  258. const ipfsHttpGatewayUrl = `http://${ipfsHost}:8080/`
  259. const { startSyncing } = require('../lib/sync')
  260. startSyncing(api, { anonymous: cli.flags.anonymous, maxSync: cli.flags.maxSync }, store)
  261. if (!cli.flags.anonymous) {
  262. announcePublicUrl(api, publicUrl)
  263. }
  264. return startAllServices({ store, api, port, ipfsHttpGatewayUrl, anonymous: cli.flags.anonymous })
  265. },
  266. }
  267. async function main() {
  268. // Simple CLI commands
  269. let command = cli.input[0]
  270. if (!command) {
  271. command = 'server'
  272. }
  273. if (Object.prototype.hasOwnProperty.call(commands, command)) {
  274. // Command recognized
  275. const args = _.clone(cli.input).slice(1)
  276. await commands[command](...args)
  277. } else {
  278. throw new Error(`Command '${command}' not recognized, aborting!`)
  279. }
  280. }
  281. main()
  282. .then(() => {
  283. process.exit(0)
  284. })
  285. .catch((err) => {
  286. console.error(chalk.red(err.stack))
  287. process.exit(-1)
  288. })