cli.js 8.2 KB

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