cli.js 8.4 KB

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