cli.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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 configstore = require('configstore');
  8. const chalk = require('chalk');
  9. const figlet = require('figlet');
  10. const _ = require('lodash');
  11. const debug = require('debug')('joystream:cli');
  12. // Project root
  13. const PROJECT_ROOT = path.resolve(__dirname, '..');
  14. // Configuration (default)
  15. const pkg = require(path.resolve(PROJECT_ROOT, 'package.json'));
  16. const default_config = new configstore(pkg.name);
  17. // Parse CLI
  18. const FLAG_DEFINITIONS = {
  19. port: {
  20. type: 'integer',
  21. alias: 'p',
  22. _default: 3000,
  23. },
  24. 'syncPeriod': {
  25. type: 'integer',
  26. _default: 120000,
  27. },
  28. keyFile: {
  29. type: 'string',
  30. },
  31. config: {
  32. type: 'string',
  33. alias: 'c',
  34. },
  35. 'publicUrl': {
  36. type: 'string',
  37. alias: 'u'
  38. },
  39. 'passphrase': {
  40. type: 'string'
  41. },
  42. 'wsProvider': {
  43. type: 'string',
  44. _default: 'ws://localhost:9944'
  45. }
  46. };
  47. const cli = meow(`
  48. Usage:
  49. $ colossus [command] [options]
  50. Commands:
  51. server [default] Run a server instance with the given configuration.
  52. signup Sign up as a storage provider. Requires that you provide
  53. a JSON account file of an account that is a member, and has
  54. sufficient balance for staking as a storage provider.
  55. Writes a new account file that should be used to run the
  56. storage node.
  57. down Signal to network that all services are down. Running
  58. the server will signal that services as online again.
  59. discovery Run the discovery service only.
  60. Options:
  61. --config=PATH, -c PATH Configuration file path. Defaults to
  62. "${default_config.path}".
  63. --port=PORT, -p PORT Port number to listen on, defaults to 3000.
  64. --sync-period Number of milliseconds to wait between synchronization
  65. runs. Defaults to 30,000 (30s).
  66. --key-file JSON key export file to use as the storage provider.
  67. --passphrase Optional passphrase to use to decrypt the key-file (if its encrypted).
  68. --public-url API Public URL to announce. No URL will be announced if not specified.
  69. --ws-provider Joystream Node websocket provider url, eg: "ws://127.0.0.1:9944"
  70. `,
  71. { flags: FLAG_DEFINITIONS });
  72. // Create configuration
  73. function create_config(pkgname, flags)
  74. {
  75. // Create defaults from flag definitions
  76. const defaults = {};
  77. for (var key in FLAG_DEFINITIONS) {
  78. const defs = FLAG_DEFINITIONS[key];
  79. if (defs._default) {
  80. defaults[key] = defs._default;
  81. }
  82. }
  83. // Provide flags as defaults. Anything stored in the config overrides.
  84. var config = new configstore(pkgname, defaults, { configPath: flags.config });
  85. // But we want the flags to also override what's stored in the config, so
  86. // set them all.
  87. for (var key in flags) {
  88. // Skip aliases and self-referential config flag
  89. if (key.length == 1 || key === 'config') continue;
  90. // Skip sensitive flags
  91. if (key == 'passphrase') continue;
  92. // Skip unset flags
  93. if (!flags[key]) continue;
  94. // Otherwise set.
  95. config.set(key, flags[key]);
  96. }
  97. debug('Configuration at', config.path, config.all);
  98. return config;
  99. }
  100. // All-important banner!
  101. function banner()
  102. {
  103. console.log(chalk.blue(figlet.textSync('joystream', 'Speed')));
  104. }
  105. function start_express_app(app, port) {
  106. const http = require('http');
  107. const server = http.createServer(app);
  108. return new Promise((resolve, reject) => {
  109. server.on('error', reject);
  110. server.on('close', (...args) => {
  111. console.log('Server closed, shutting down...');
  112. resolve(...args);
  113. });
  114. server.on('listening', () => {
  115. console.log('API server started.', server.address());
  116. });
  117. server.listen(port, '::');
  118. console.log('Starting API server...');
  119. });
  120. }
  121. // Start app
  122. function start_all_services(store, api, config)
  123. {
  124. const app = require('../lib/app')(PROJECT_ROOT, store, api, config);
  125. const port = config.get('port');
  126. return start_express_app(app, port);
  127. }
  128. // Start discovery service app
  129. function start_discovery_service(api, config)
  130. {
  131. const app = require('../lib/discovery')(PROJECT_ROOT, api, config);
  132. const port = config.get('port');
  133. return start_express_app(app, port);
  134. }
  135. // Get an initialized storage instance
  136. function get_storage(runtime_api, config)
  137. {
  138. // TODO at some point, we can figure out what backend-specific connection
  139. // options make sense. For now, just don't use any configuration.
  140. const { Storage } = require('@joystream/storage');
  141. const options = {
  142. resolve_content_id: async (content_id) => {
  143. // Resolve via API
  144. const obj = await runtime_api.assets.getDataObject(content_id);
  145. if (!obj || obj.isNone) {
  146. return;
  147. }
  148. return obj.unwrap().ipfs_content_id.toString();
  149. },
  150. };
  151. return Storage.create(options);
  152. }
  153. async function run_signup(account_file, provider_url)
  154. {
  155. if (!account_file) {
  156. console.log('Cannot proceed without keyfile');
  157. return
  158. }
  159. const { RuntimeApi } = require('@joystream/runtime-api');
  160. const api = await RuntimeApi.create({account_file, canPromptForPassphrase: true, provider_url});
  161. if (!api.identities.key) {
  162. console.log('Cannot proceed without a member account');
  163. return
  164. }
  165. // Check there is an opening
  166. let availableSlots = await api.roles.availableSlotsForRole(api.roles.ROLE_STORAGE);
  167. if (availableSlots == 0) {
  168. console.log(`
  169. There are no open storage provider slots available at this time.
  170. Please try again later.
  171. `);
  172. return;
  173. } else {
  174. console.log(`There are still ${availableSlots} slots available, proceeding`);
  175. }
  176. const member_address = api.identities.key.address;
  177. // Check if account works
  178. const min = await api.roles.requiredBalanceForRoleStaking(api.roles.ROLE_STORAGE);
  179. console.log(`Account needs to be a member and have a minimum balance of ${min.toString()}`);
  180. const check = await api.roles.checkAccountForStaking(member_address);
  181. if (check) {
  182. console.log('Account is working for staking, proceeding.');
  183. }
  184. // Create a role key
  185. const role_key = await api.identities.createRoleKey(member_address);
  186. const role_address = role_key.address;
  187. console.log('Generated', role_address, '- this is going to be exported to a JSON file.\n',
  188. ' You can provide an empty passphrase to make starting the server easier,\n',
  189. ' but you must keep the file very safe, then.');
  190. const filename = await api.identities.writeKeyPairExport(role_address);
  191. console.log('Identity stored in', filename);
  192. // Ok, transfer for staking.
  193. await api.roles.transferForStaking(member_address, role_address, api.roles.ROLE_STORAGE);
  194. console.log('Funds transferred.');
  195. // Now apply for the role
  196. await api.roles.applyForRole(role_address, api.roles.ROLE_STORAGE, member_address);
  197. console.log('Role application sent.\nNow visit Roles > My Requests in the app.');
  198. }
  199. async function wait_for_role(config)
  200. {
  201. // Load key information
  202. const { RuntimeApi } = require('@joystream/runtime-api');
  203. const keyFile = config.get('keyFile');
  204. if (!keyFile) {
  205. throw new Error("Must specify a key file for running a storage node! Sign up for the role; see `colussus --help' for details.");
  206. }
  207. const wsProvider = config.get('wsProvider');
  208. const api = await RuntimeApi.create({
  209. account_file: keyFile,
  210. passphrase: cli.flags.passphrase,
  211. provider_url: wsProvider,
  212. });
  213. if (!api.identities.key) {
  214. throw new Error('Failed to unlock storage provider account');
  215. }
  216. // Wait for the account role to be finalized
  217. console.log('Waiting for the account to be staked as a storage provider role...');
  218. const result = await api.roles.waitForRole(api.identities.key.address, api.roles.ROLE_STORAGE);
  219. return [result, api];
  220. }
  221. function get_service_information(config) {
  222. // For now assume we run all services on the same endpoint
  223. return({
  224. asset: {
  225. version: 1, // spec version
  226. endpoint: config.get('publicUrl')
  227. },
  228. discover: {
  229. version: 1, // spec version
  230. endpoint: config.get('publicUrl')
  231. }
  232. })
  233. }
  234. async function announce_public_url(api, config) {
  235. // re-announce in future
  236. const reannounce = function (timeoutMs) {
  237. setTimeout(announce_public_url, timeoutMs, api, config);
  238. }
  239. debug('announcing public url')
  240. const { publish } = require('@joystream/discovery')
  241. const accountId = api.identities.key.address
  242. try {
  243. const serviceInformation = get_service_information(config)
  244. let keyId = await publish.publish(serviceInformation);
  245. const expiresInBlocks = 600; // ~ 1 hour (6s block interval)
  246. await api.discovery.setAccountInfo(accountId, keyId, expiresInBlocks);
  247. debug('publishing complete, scheduling next update')
  248. // >> sometimes after tx is finalized.. we are not reaching here!
  249. // Reannounce before expiery
  250. reannounce(50 * 60 * 1000); // in 50 minutes
  251. } catch (err) {
  252. debug(`announcing public url failed: ${err.stack}`)
  253. // On failure retry sooner
  254. debug(`announcing failed, retrying in: 2 minutes`)
  255. reannounce(120 * 1000)
  256. }
  257. }
  258. function go_offline(api) {
  259. return api.discovery.unsetAccountInfo(api.identities.key.address)
  260. }
  261. // Simple CLI commands
  262. var command = cli.input[0];
  263. if (!command) {
  264. command = 'server';
  265. }
  266. const commands = {
  267. 'server': async () => {
  268. const cfg = create_config(pkg.name, cli.flags);
  269. // Load key information
  270. const values = await wait_for_role(cfg);
  271. const result = values[0]
  272. const api = values[1];
  273. if (!result) {
  274. throw new Error(`Not staked as storage role.`);
  275. }
  276. console.log('Staked, proceeding.');
  277. // Make sure a public URL is configured
  278. if (!cfg.get('publicUrl')) {
  279. throw new Error('publicUrl not configured')
  280. }
  281. // Continue with server setup
  282. const store = get_storage(api, cfg);
  283. banner();
  284. const { start_syncing } = require('../lib/sync');
  285. start_syncing(api, cfg, store);
  286. announce_public_url(api, cfg);
  287. await start_all_services(store, api, cfg);
  288. },
  289. 'signup': async (account_file) => {
  290. const cfg = create_config(pkg.name, cli.flags);
  291. await run_signup(account_file, cfg.get('wsProvider'));
  292. },
  293. 'down': async () => {
  294. const cfg = create_config(pkg.name, cli.flags);
  295. const values = await wait_for_role(cfg);
  296. const result = values[0]
  297. const api = values[1];
  298. if (!result) {
  299. throw new Error(`Not staked as storage role.`);
  300. }
  301. await go_offline(api)
  302. },
  303. 'discovery': async () => {
  304. debug("Starting Joystream Discovery Service")
  305. const { RuntimeApi } = require('@joystream/runtime-api')
  306. const cfg = create_config(pkg.name, cli.flags)
  307. const wsProvider = cfg.get('wsProvider');
  308. const api = await RuntimeApi.create({ provider_url: wsProvider });
  309. await start_discovery_service(api, cfg)
  310. }
  311. };
  312. async function main()
  313. {
  314. // Simple CLI commands
  315. var command = cli.input[0];
  316. if (!command) {
  317. command = 'server';
  318. }
  319. if (commands.hasOwnProperty(command)) {
  320. // Command recognized
  321. const args = _.clone(cli.input).slice(1);
  322. await commands[command](...args);
  323. }
  324. else {
  325. throw new Error(`Command "${command}" not recognized, aborting!`);
  326. }
  327. }
  328. main()
  329. .then(() => {
  330. console.log('Process exiting gracefully.');
  331. process.exit(0);
  332. })
  333. .catch((err) => {
  334. console.error(chalk.red(err.stack));
  335. process.exit(-1);
  336. });