cli.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. (discovery and storage services)
  78. This is the default command if not specified.
  79. discovery Run the discovery service only.
  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. `,
  93. { flags: FLAG_DEFINITIONS }
  94. )
  95. // All-important banner!
  96. function banner() {
  97. console.log(chalk.blue(figlet.textSync('joystream', 'Speed')))
  98. }
  99. function startExpressApp(app, port) {
  100. const http = require('http')
  101. const server = http.createServer(app)
  102. return new Promise((resolve, reject) => {
  103. server.on('error', reject)
  104. server.on('close', (...args) => {
  105. console.log('Server closed, shutting down...')
  106. resolve(...args)
  107. })
  108. server.on('listening', () => {
  109. console.log('API server started.', server.address())
  110. })
  111. server.listen(port, '::')
  112. console.log('Starting API server...')
  113. })
  114. }
  115. // Start app
  116. function startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl, anonymous }) {
  117. const app = require('../lib/app')(PROJECT_ROOT, store, api, discoveryClient, ipfsHttpGatewayUrl, anonymous)
  118. return startExpressApp(app, port)
  119. }
  120. // Start discovery service app only
  121. function startDiscoveryService({ port, discoveryClient }) {
  122. const app = require('../lib/discovery')(PROJECT_ROOT, discoveryClient)
  123. return startExpressApp(app, port)
  124. }
  125. // Get an initialized storage instance
  126. function getStorage(runtimeApi, { ipfsHost }) {
  127. // TODO at some point, we can figure out what backend-specific connection
  128. // options make sense. For now, just don't use any configuration.
  129. const { Storage } = require('@joystream/storage-node-backend')
  130. const options = {
  131. resolve_content_id: async (contentId) => {
  132. // Resolve via API
  133. const obj = await runtimeApi.assets.getDataObject(contentId)
  134. if (!obj || obj.isNone) {
  135. return
  136. }
  137. // if obj.liaison_judgement !== Accepted .. throw ?
  138. return obj.unwrap().ipfs_content_id.toString()
  139. },
  140. ipfsHost,
  141. }
  142. return Storage.create(options)
  143. }
  144. async function initApiProduction({ wsProvider, providerId, keyFile, passphrase, anonymous }) {
  145. // Load key information
  146. const { RuntimeApi } = require('@joystream/storage-runtime-api')
  147. const api = await RuntimeApi.create({
  148. account_file: keyFile,
  149. passphrase,
  150. provider_url: wsProvider,
  151. storageProviderId: providerId,
  152. })
  153. if (!anonymous && !api.identities.key) {
  154. throw new Error('Failed to unlock storage provider account')
  155. }
  156. await api.untilChainIsSynced()
  157. // We allow the node to startup without correct provider id and account, but syncing and
  158. // publishing of identity will be skipped.
  159. if (!anonymous && !(await api.providerIsActiveWorker())) {
  160. debug('storage provider role account and storageProviderId are not associated with a worker')
  161. }
  162. return api
  163. }
  164. async function initApiDevelopment({ wsProvider }) {
  165. // Load key information
  166. const { RuntimeApi } = require('@joystream/storage-runtime-api')
  167. const api = await RuntimeApi.create({
  168. provider_url: wsProvider,
  169. })
  170. const dev = require('../../cli/dist/commands/dev')
  171. api.identities.useKeyPair(dev.roleKeyPair(api))
  172. // Wait until dev provider is added to role
  173. while (true) {
  174. try {
  175. api.storageProviderId = await dev.check(api)
  176. break
  177. } catch (err) {
  178. debug(err)
  179. }
  180. await sleep(10000)
  181. }
  182. return api
  183. }
  184. function getServiceInformation(publicUrl) {
  185. // For now assume we run all services on the same endpoint
  186. return {
  187. asset: {
  188. version: 1, // spec version
  189. endpoint: publicUrl,
  190. },
  191. discover: {
  192. version: 1, // spec version
  193. endpoint: publicUrl,
  194. },
  195. }
  196. }
  197. // TODO: instead of recursion use while/async-await and use promise/setTimout based sleep
  198. // or cleaner code with generators?
  199. async function announcePublicUrl(api, publicUrl, publisherClient) {
  200. // re-announce in future
  201. const reannounce = function (timeoutMs) {
  202. setTimeout(announcePublicUrl, timeoutMs, api, publicUrl, publisherClient)
  203. }
  204. const chainIsSyncing = await api.chainIsSyncing()
  205. if (chainIsSyncing) {
  206. debug('Chain is syncing. Postponing announcing public url.')
  207. return reannounce(10 * 60 * 1000)
  208. }
  209. // postpone if provider not active
  210. if (!(await api.providerIsActiveWorker())) {
  211. debug('storage provider role account and storageProviderId are not associated with a worker')
  212. return reannounce(10 * 60 * 1000)
  213. }
  214. const sufficientBalance = await api.providerHasMinimumBalance(1)
  215. if (!sufficientBalance) {
  216. debug('Provider role account does not have sufficient balance. Postponing announcing public url.')
  217. return reannounce(10 * 60 * 1000)
  218. }
  219. debug('announcing public url')
  220. try {
  221. const serviceInformation = getServiceInformation(publicUrl)
  222. const keyId = await publisherClient.publish(serviceInformation)
  223. await api.discovery.setAccountInfo(keyId)
  224. debug('publishing complete, scheduling next update')
  225. // >> sometimes after tx is finalized.. we are not reaching here!
  226. // Reannounce before expiery. Here we are concerned primarily
  227. // with keeping the account information refreshed and 'available' in
  228. // the ipfs network. our record on chain is valid for 24hr
  229. reannounce(50 * 60 * 1000) // in 50 minutes
  230. } catch (err) {
  231. debug(`announcing public url failed: ${err.stack}`)
  232. // On failure retry sooner
  233. debug(`announcing failed, retrying in: 2 minutes`)
  234. reannounce(120 * 1000)
  235. }
  236. }
  237. // Simple CLI commands
  238. let command = cli.input[0]
  239. if (!command) {
  240. command = 'server'
  241. }
  242. const commands = {
  243. server: async () => {
  244. banner()
  245. let publicUrl, port, api
  246. if (cli.flags.dev) {
  247. const dev = require('../../cli/dist/commands/dev')
  248. api = await initApiDevelopment(cli.flags)
  249. port = dev.developmentPort()
  250. publicUrl = `http://localhost:${port}/`
  251. } else {
  252. api = await initApiProduction(cli.flags)
  253. publicUrl = cli.flags.publicUrl
  254. port = cli.flags.port
  255. }
  256. // TODO: check valid url, and valid port number
  257. const store = getStorage(api, cli.flags)
  258. const ipfsHost = cli.flags.ipfsHost
  259. const ipfs = require('ipfs-http-client')(ipfsHost, '5001', { protocol: 'http' })
  260. const ipfsHttpGatewayUrl = `http://${ipfsHost}:8080/`
  261. const { startSyncing } = require('../lib/sync')
  262. startSyncing(api, { syncPeriod: SYNC_PERIOD_MS, anonymous: cli.flags.anonymous }, store)
  263. if (!cli.flags.anonymous) {
  264. const { PublisherClient } = require('@joystream/service-discovery')
  265. announcePublicUrl(api, publicUrl, new PublisherClient(ipfs))
  266. }
  267. const { DiscoveryClient } = require('@joystream/service-discovery')
  268. const discoveryClient = new DiscoveryClient({ ipfs, api })
  269. return startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl, anonymous: cli.flags.anonymous })
  270. },
  271. discovery: async () => {
  272. banner()
  273. debug('Starting Joystream Discovery Service')
  274. const { RuntimeApi } = require('@joystream/storage-runtime-api')
  275. const wsProvider = cli.flags.wsProvider
  276. const api = await RuntimeApi.create({ provider_url: wsProvider })
  277. const port = cli.flags.port
  278. const ipfsHost = cli.flags.ipfsHost
  279. const ipfs = require('ipfs-http-client')(ipfsHost, '5001', { protocol: 'http' })
  280. const { DiscoveryClient } = require('@joystream/service-discovery')
  281. const discoveryClient = new DiscoveryClient({ ipfs, api })
  282. await api.untilChainIsSynced()
  283. await startDiscoveryService({ api, port, discoveryClient })
  284. },
  285. }
  286. async function main() {
  287. // Simple CLI commands
  288. let command = cli.input[0]
  289. if (!command) {
  290. command = 'server'
  291. }
  292. if (Object.prototype.hasOwnProperty.call(commands, command)) {
  293. // Command recognized
  294. const args = _.clone(cli.input).slice(1)
  295. await commands[command](...args)
  296. } else {
  297. throw new Error(`Command '${command}' not recognized, aborting!`)
  298. }
  299. }
  300. main()
  301. .then(() => {
  302. process.exit(0)
  303. })
  304. .catch((err) => {
  305. console.error(chalk.red(err.stack))
  306. process.exit(-1)
  307. })