cli.js 8.4 KB

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