diff --git a/app/cli.js b/app/cli.js index d581b6fa01d3c26bd060b577657b67b407f3dfa8..c3260ec82c7f07f881a03bfd370a1d25d50c4382 100644 --- a/app/cli.js +++ b/app/cli.js @@ -5,7 +5,7 @@ const logger = require('../app/lib/logger')('cli'); const async = require('async'); const Q = require('q'); const _ = require('underscore'); -const program = require('commander'); +const Command = require('commander').Command; const contacter = require('../app/lib/contacter'); const directory = require('../app/lib/system/directory'); const wizard = require('../app/lib/wizard'); @@ -18,306 +18,516 @@ const Peer = require('../app/lib/entity/peer'); const Block = require('../app/lib/entity/block'); const constants = require('../app/lib/constants'); -let currentCommand = Promise.resolve(true); +module.exports = () => { -let onResolve, onReject, onService, closeCommand = () => Promise.resolve(true); + const ERASE_IF_ALREADY_RECORDED = true; + const NO_LOGS = true; -module.exports = (programArgs) => { + const options = []; + const commands = []; - currentCommand = new Promise((resolve, reject) => { - onResolve = resolve; - onReject = reject; - }); - return { - // Some external event can trigger the program closing function - closeCommand: () => closeCommand(), + addOption: (optFormat, optDesc, optParser) => options.push({ optFormat, optDesc, optParser }), + + addCommand: (command, executionCallback) => commands.push({ command, executionCallback }), // To execute the provided command - execute: (onServiceCallback) => co(function*() { + execute: (programArgs, onServiceCallback) => co(function*() { - onService = onServiceCallback; - program.parse(programArgs); + const program = new Command(); - if (programArgs.length <= 2) { - onReject('No command given.'); - } + let onResolve, onReject = () => Promise.reject(Error("Uninitilized rejection throw")), onService, closeCommand = () => Promise.resolve(true); + const currentCommand = new Promise((resolve, reject) => { + onResolve = resolve; + onReject = reject; + }); - const res = yield currentCommand; - if (closeCommand) { - yield closeCommand(); + program + .version(pjson.version) + .usage('<command> [options]') + + .option('--home <path>', 'Path to Duniter HOME (defaults to "$HOME/.config/duniter").') + .option('-d, --mdb <name>', 'Database name (defaults to "duniter_default").') + + .option('--autoconf', 'With `config` and `init` commands, will guess the best network and key options witout asking for confirmation') + .option('--ipv4 <address>', 'IPv4 interface to listen for requests') + .option('--ipv6 <address>', 'IPv6 interface to listen for requests') + .option('--remoteh <host>', 'Remote interface others may use to contact this node') + .option('--remote4 <host>', 'Remote interface for IPv4 access') + .option('--remote6 <host>', 'Remote interface for IPv6 access') + .option('-p, --port <port>', 'Port to listen for requests', parseInt) + .option('--remotep <port>', 'Remote port others may use to contact this node') + .option('--upnp', 'Use UPnP to open remote port') + .option('--noupnp', 'Do not use UPnP to open remote port') + .option('--addep <endpoint>', 'With `config` command, add given endpoint to the list of endpoints of this node') + .option('--remep <endpoint>', 'With `config` command, remove given endpoint to the list of endpoints of this node') + + .option('--salt <salt>', 'Key salt to generate this key\'s secret key') + .option('--passwd <password>', 'Password to generate this key\'s secret key') + .option('--participate <Y|N>', 'Participate to writing the blockchain') + .option('--cpu <percent>', 'Percent of CPU usage for proof-of-work computation', parsePercent) + + .option('-c, --currency <name>', 'Name of the currency managed by this node.') + .option('--sigPeriod <timestamp>', 'Minimum delay between 2 certifications of a same issuer, in seconds.') + .option('--sigStock <count>', 'Maximum quantity of valid certifications per member.') + .option('--sigWindow <duration>', 'Maximum age of a non-written certification.') + .option('--idtyWindow <duration>', 'Maximum age of a non-written certification.') + .option('--sigValidity <timestamp>', 'Validity duration of a certification, in seconds.') + .option('--msValidity <timestamp>', 'Validity duration of a memberships, in seconds.') + .option('--sigQty <number>', 'Minimum number of required certifications to be a member/stay as a member') + .option('--medtblocks <number>', 'medianTimeBlocks parameter of UCP') + .option('--avgGenTime <number>', 'avgGenTime parameter of UCP') + .option('--dtdiffeval <number>', 'dtDiffEval parameter of UCP') + .option('--powZeroMin <number>', 'Minimum number of leading zeros for a proof-of-work') + .option('--powPeriod <number>', 'Number of blocks to wait to decrease proof-of-work difficulty by one') + .option('--powDelay <number>', 'Number of seconds to wait before starting the computation of next block') + .option('--growth <number>', 'Universal Dividend %growth. Aka. \'c\' parameter in RTM', parsePercent) + .option('--ud0 <number>', 'Universal Dividend initial value') + .option('--dt <number>', 'Number of seconds between two UD') + .option('--rootoffset <number>', 'Allow to give a time offset for first block (offset in the past)') + .option('--show', 'With gen-next or gen-root commands, displays the generated block') + + .option('--nointeractive', 'Disable interactive sync UI') + .option('--nocautious', 'Do not check blocks validity during sync') + .option('--cautious', 'Check blocks validity during sync (overrides --nocautious option)') + .option('--nopeers', 'Do not retrieve peers during sync') + .option('--nostdout', 'Disable stdout printing for `export-bc` command') + .option('--noshuffle', 'Disable peers shuffling for `sync` command') + + .option('--timeout <milliseconds>', 'Timeout to use when contacting peers', parseInt) + .option('--httplogs', 'Enable HTTP logs') + .option('--nohttplogs', 'Disable HTTP logs') + .option('--isolate', 'Avoid the node to send peering or status informations to the network') + .option('--check', 'With gen-next: just check validity of generated block') + .option('--forksize <size>', 'Maximum size of fork window', parseInt) + .option('--memory', 'Memory mode') + ; + + for (const opt of options) { + program + .option(opt.optFormat, opt.optDesc, opt.optParser); } - return res; - }) - }; -}; -function subCommand(promiseFunc) { - return function() { - let args = Array.prototype.slice.call(arguments, 0); - return co(function*() { - try { - let result = yield promiseFunc.apply(null, args); - onResolve(result); - } catch (e) { - if (e && e.uerr) { - onReject(e.uerr.message); - } else { - onReject(e); - } + for (const cmd of commands) { + program + .command(cmd.command.name) + .description(cmd.command.desc) + .action((...args) => co(function*() { + try { + const res = yield cmd.executionCallback.apply(null, [program].concat(args)); + onResolve(res); + } catch (e) { + onReject(e); + } + })); } - }) - }; -} -const ERASE_IF_ALREADY_RECORDED = true; -const NO_LOGS = true; - -program - .version(pjson.version) - .usage('<command> [options]') - - .option('--home <path>', 'Path to Duniter HOME (defaults to "$HOME/.config/duniter").') - .option('-d, --mdb <name>', 'Database name (defaults to "duniter_default").') - - .option('--autoconf', 'With `config` and `init` commands, will guess the best network and key options witout asking for confirmation') - .option('--ipv4 <address>', 'IPv4 interface to listen for requests') - .option('--ipv6 <address>', 'IPv6 interface to listen for requests') - .option('--remoteh <host>', 'Remote interface others may use to contact this node') - .option('--remote4 <host>', 'Remote interface for IPv4 access') - .option('--remote6 <host>', 'Remote interface for IPv6 access') - .option('-p, --port <port>', 'Port to listen for requests', parseInt) - .option('--remotep <port>', 'Remote port others may use to contact this node') - .option('--upnp', 'Use UPnP to open remote port') - .option('--noupnp', 'Do not use UPnP to open remote port') - .option('--addep <endpoint>', 'With `config` command, add given endpoint to the list of endpoints of this node') - .option('--remep <endpoint>', 'With `config` command, remove given endpoint to the list of endpoints of this node') - - .option('--salt <salt>', 'Key salt to generate this key\'s secret key') - .option('--passwd <password>', 'Password to generate this key\'s secret key') - .option('--participate <Y|N>', 'Participate to writing the blockchain') - .option('--cpu <percent>', 'Percent of CPU usage for proof-of-work computation', parsePercent) - - .option('-c, --currency <name>', 'Name of the currency managed by this node.') - .option('--sigPeriod <timestamp>', 'Minimum delay between 2 certifications of a same issuer, in seconds.') - .option('--sigStock <count>', 'Maximum quantity of valid certifications per member.') - .option('--sigWindow <duration>', 'Maximum age of a non-written certification.') - .option('--idtyWindow <duration>', 'Maximum age of a non-written certification.') - .option('--sigValidity <timestamp>', 'Validity duration of a certification, in seconds.') - .option('--msValidity <timestamp>', 'Validity duration of a memberships, in seconds.') - .option('--sigQty <number>', 'Minimum number of required certifications to be a member/stay as a member') - .option('--medtblocks <number>', 'medianTimeBlocks parameter of UCP') - .option('--avgGenTime <number>', 'avgGenTime parameter of UCP') - .option('--dtdiffeval <number>', 'dtDiffEval parameter of UCP') - .option('--powZeroMin <number>', 'Minimum number of leading zeros for a proof-of-work') - .option('--powPeriod <number>', 'Number of blocks to wait to decrease proof-of-work difficulty by one') - .option('--powDelay <number>', 'Number of seconds to wait before starting the computation of next block') - .option('--growth <number>', 'Universal Dividend %growth. Aka. \'c\' parameter in RTM', parsePercent) - .option('--ud0 <number>', 'Universal Dividend initial value') - .option('--dt <number>', 'Number of seconds between two UD') - .option('--rootoffset <number>', 'Allow to give a time offset for first block (offset in the past)') - .option('--show', 'With gen-next or gen-root commands, displays the generated block') - - .option('--nointeractive', 'Disable interactive sync UI') - .option('--nocautious', 'Do not check blocks validity during sync') - .option('--cautious', 'Check blocks validity during sync (overrides --nocautious option)') - .option('--nopeers', 'Do not retrieve peers during sync') - .option('--nostdout', 'Disable stdout printing for `export-bc` command') - .option('--noshuffle', 'Disable peers shuffling for `sync` command') - - .option('--timeout <milliseconds>', 'Timeout to use when contacting peers', parseInt) - .option('--httplogs', 'Enable HTTP logs') - .option('--nohttplogs', 'Disable HTTP logs') - .option('--isolate', 'Avoid the node to send peering or status informations to the network') - .option('--check', 'With gen-next: just check validity of generated block') - .option('--forksize <size>', 'Maximum size of fork window', parseInt) - .option('--memory', 'Memory mode') -; - -program - .command('start') - .description('Start Duniter node daemon.') - .action(subCommand(service((server, conf) => new Promise((resolve, reject) => { - co(function*() { - try { - const bma = require('./lib/streams/bma'); + program + .command('start') + .description('Start Duniter node daemon.') + .action(subCommand(service((server, conf) => new Promise((resolve, reject) => { + co(function*() { + try { + const bma = require('./lib/streams/bma'); - logger.info(">> NODE STARTING"); + logger.info(">> NODE STARTING"); - // Public http interface - let bmapi = yield bma(server, null, conf.httplogs); + // Public http interface + let bmapi = yield bma(server, null, conf.httplogs); - // Routing documents - server.routing(); + // Routing documents + server.routing(); - // Services - yield server.startServices(); - yield bmapi.openConnections(); + // Services + yield server.startServices(); + yield bmapi.openConnections(); - logger.info('>> Server ready!'); + logger.info('>> Server ready!'); - } catch (e) { - reject(e); - } - }); - })))); - -program - .command('stop') - .description('Stop Duniter node daemon.') - .action(subCommand(needsToBeLaunchedByScript)); - -program - .command('restart') - .description('Restart Duniter node daemon.') - .action(subCommand(needsToBeLaunchedByScript)); - -program - .command('wizard [step]') - .description('Launch the configuration wizard.') - .action(subCommand(function (step) { - // Only show message "Saved" - return connect(function (step, server, conf) { - return new Promise((resolve, reject) => { - async.series([ - function (next) { - startWizard(step, server, conf, next); + } catch (e) { + reject(e); + } + }); + })))); + + program + .command('stop') + .description('Stop Duniter node daemon.') + .action(subCommand(needsToBeLaunchedByScript)); + + program + .command('restart') + .description('Restart Duniter node daemon.') + .action(subCommand(needsToBeLaunchedByScript)); + + program + .command('wizard [step]') + .description('Launch the configuration wizard.') + .action(subCommand(function (step) { + // Only show message "Saved" + return connect(function (step, server, conf) { + return new Promise((resolve, reject) => { + async.series([ + function (next) { + startWizard(service, step, server, conf, next); + } + ], (err) => { + if (err) return reject(err); + resolve(); + }); + }); + })(step, null); + })); + + program + .command('sync [host] [port] [to]') + .description('Synchronize blockchain from a remote Duniter node') + .action(subCommand(service(function (host, port, to, server, conf) { + if (!host) { + throw 'Host is required.'; + } + if (!port) { + throw 'Port is required.'; + } + return co(function *() { + let cautious; + if (program.nocautious) { + cautious = false; + } + if (program.cautious) { + cautious = true; + } + yield server.synchronize(host, port, parseInt(to), 0, !program.nointeractive, cautious, program.nopeers, program.noshuffle); + if (server) { + yield server.disconnect(); + } + }); + }))); + + program + .command('peer [host] [port]') + .description('Exchange peerings with another node') + .action(subCommand(service(function (host, port, server) { + return co(function *() { + try { + logger.info('Fetching peering record at %s:%s...', host, port); + let peering = yield contacter.statics.fetchPeer(host, port); + logger.info('Apply peering ...'); + yield server.PeeringService.submitP(peering, ERASE_IF_ALREADY_RECORDED, !program.nocautious); + logger.info('Applied'); + let selfPeer = yield server.dal.getPeer(server.PeeringService.pubkey); + if (!selfPeer) { + yield Q.nfcall(server.PeeringService.generateSelfPeer, server.conf, 0); + selfPeer = yield server.dal.getPeer(server.PeeringService.pubkey); + } + logger.info('Send self peering ...'); + var caster = multicaster(); + yield caster.sendPeering(Peer.statics.peerize(peering), Peer.statics.peerize(selfPeer)); + logger.info('Sent.'); + yield server.disconnect(); + } catch(e) { + logger.error(e.code || e.message || e); + throw Error("Exiting"); + } + }); + }))); + + program + .command('revert [count]') + .description('Revert (undo + remove) the top [count] blocks from the blockchain. EXPERIMENTAL') + .action(subCommand(service(function (count, server) { + return co(function *() { + try { + for (let i = 0; i < count; i++) { + yield server.revert(); + } + } catch (err) { + logger.error('Error during revert:', err); + } + // Save DB + yield server.disconnect(); + }); + }))); + + program + .command('revert-to [number]') + .description('Revert (undo + remove) top blockchain blocks until block #[number] is reached. EXPERIMENTAL') + .action(subCommand(service(function (number, server) { + return co(function *() { + try { + yield server.revertTo(number); + } catch (err) { + logger.error('Error during revert:', err); + } + // Save DB + if (server) { + yield server.disconnect(); + } + }); + }))); + + program + .command('reapply-to [number]') + .description('Reapply reverted blocks until block #[number] is reached. EXPERIMENTAL') + .action(subCommand(service(function (number, server) { + return co(function *() { + try { + yield server.reapplyTo(number); + } catch (err) { + logger.error('Error during reapply:', err); + } + // Save DB + if (server) { + yield server.disconnect(); + } + }); + }))); + + program + .command('gen-next [host] [port] [difficulty]') + .description('Tries to generate the next block of the blockchain') + .action(subCommand(service(generateAndSend(program, (server) => server.BlockchainService.generateNext)))); + + program + .command('gen-root [host] [port] [difficulty]') + .description('Tries to generate root block, with choice of root members') + .action(subCommand(service(function (host, port, difficulty, server, conf) { + if (!host) { + throw 'Host is required.'; + } + if (!port) { + throw 'Port is required.'; } - ], (err) => { - if (err) return reject(err); - resolve(); + if (!difficulty) { + throw 'Difficulty is required.'; + } + return generateAndSend(program, (server) => server.BlockchainService.generateManualRoot)(host, port, difficulty, server, conf); + }))); + + program + .command('export-bc [upto]') + .description('Exports the whole blockchain as JSON array, up to [upto] block number (excluded).') + .action(subCommand(service(function (upto, server) { + return co(function *() { + try { + let CHUNK_SIZE = 500; + let jsoned = []; + let current = yield server.dal.getCurrentBlockOrNull(); + let lastNumber = current ? current.number + 1 : -1; + if (upto !== undefined && upto.match(/\d+/)) { + lastNumber = Math.min(parseInt(upto), lastNumber); + } + let chunksCount = Math.floor(lastNumber / CHUNK_SIZE); + let chunks = []; + // Max-size chunks + for (let i = 0, len = chunksCount; i < len; i++) { + chunks.push({start: i * CHUNK_SIZE, to: i * CHUNK_SIZE + CHUNK_SIZE - 1}); + } + // A last chunk + if (lastNumber > chunksCount * CHUNK_SIZE) { + chunks.push({start: chunksCount * CHUNK_SIZE, to: lastNumber}); + } + for (const chunk of chunks) { + let blocks = yield server.dal.getBlocksBetween(chunk.start, chunk.to); + blocks.forEach(function (block) { + jsoned.push(_(new Block(block).json()).omit('raw')); + }); + } + if (!program.nostdout) { + console.log(JSON.stringify(jsoned, null, " ")); + } + yield server.disconnect(); + return jsoned; + } catch(err) { + logger.warn(err.message || err); + yield server.disconnect(); + } + }); + }, NO_LOGS))); + + program + .command('check-config') + .description('Checks the node\'s configuration') + .action(subCommand(service(function (server) { + return server.checkConfig() + .then(function () { + logger.warn('Configuration seems correct.'); + }) + }))); + + program + .command('reset [config|data|peers|tx|stats|all]') + .description('Reset configuration, data, peers, transactions or everything in the database') + .action(subCommand((type) => { + let init = ['data', 'all'].indexOf(type) !== -1 ? server.bind(server, program) : connect; + return init(function (server) { + if (!~['config', 'data', 'peers', 'stats', 'all'].indexOf(type)) { + throw constants.ERRORS.CLI_CALLERR_RESET; + } + return co(function*() { + try { + if (type == 'data') { + yield server.resetData(); + logger.warn('Data successfully reseted.'); + } + if (type == 'peers') { + yield server.resetPeers(); + logger.warn('Peers successfully reseted.'); + } + if (type == 'stats') { + yield server.resetStats(); + logger.warn('Stats successfully reseted.'); + } + if (type == 'config') { + yield server.resetConf(); + logger.warn('Configuration successfully reseted.'); + } + if (type == 'all') { + yield server.resetAll(); + logger.warn('Data & Configuration successfully reseted.'); + } + } catch (e) { + logger.error(e); + } + }); + }, type != 'peers')(type); + })); + + program + .on('*', function (cmd) { + console.log("Unknown command '%s'. Try --help for a listing of commands & options.", cmd); + onResolve(); }); - }); - })(step, null); - })); - -program - .command('sync [host] [port] [to]') - .description('Synchronize blockchain from a remote Duniter node') - .action(subCommand(service(function (host, port, to, server, conf) { - if (!host) { - throw 'Host is required.'; - } - if (!port) { - throw 'Port is required.'; - } - return co(function *() { - let cautious; - if (program.nocautious) { - cautious = false; - } - if (program.cautious) { - cautious = true; - } - yield server.synchronize(host, port, parseInt(to), 0, !program.nointeractive, cautious, program.nopeers, program.noshuffle); - if (server) { - yield server.disconnect(); - } - }); - }))); - -program - .command('peer [host] [port]') - .description('Exchange peerings with another node') - .action(subCommand(service(function (host, port, server) { - return co(function *() { - try { - logger.info('Fetching peering record at %s:%s...', host, port); - let peering = yield contacter.statics.fetchPeer(host, port); - logger.info('Apply peering ...'); - yield server.PeeringService.submitP(peering, ERASE_IF_ALREADY_RECORDED, !program.nocautious); - logger.info('Applied'); - let selfPeer = yield server.dal.getPeer(server.PeeringService.pubkey); - if (!selfPeer) { - yield Q.nfcall(server.PeeringService.generateSelfPeer, server.conf, 0); - selfPeer = yield server.dal.getPeer(server.PeeringService.pubkey); - } - logger.info('Send self peering ...'); - var caster = multicaster(); - yield caster.sendPeering(Peer.statics.peerize(peering), Peer.statics.peerize(selfPeer)); - logger.info('Sent.'); - yield server.disconnect(); - } catch(e) { - logger.error(e.code || e.message || e); - throw Error("Exiting"); - } - }); - }))); - -program - .command('revert [count]') - .description('Revert (undo + remove) the top [count] blocks from the blockchain. EXPERIMENTAL') - .action(subCommand(service(function (count, server) { - return co(function *() { - try { - for (let i = 0; i < count; i++) { - yield server.revert(); - } - } catch (err) { - logger.error('Error during revert:', err); - } - // Save DB - yield server.disconnect(); - }); - }))); - -program - .command('revert-to [number]') - .description('Revert (undo + remove) top blockchain blocks until block #[number] is reached. EXPERIMENTAL') - .action(subCommand(service(function (number, server) { - return co(function *() { - try { - yield server.revertTo(number); - } catch (err) { - logger.error('Error during revert:', err); - } - // Save DB - if (server) { - yield server.disconnect(); - } - }); - }))); - -program - .command('reapply-to [number]') - .description('Reapply reverted blocks until block #[number] is reached. EXPERIMENTAL') - .action(subCommand(service(function (number, server) { - return co(function *() { - try { - yield server.reapplyTo(number); - } catch (err) { - logger.error('Error during reapply:', err); + + function subCommand(promiseFunc) { + return function() { + let args = Array.prototype.slice.call(arguments, 0); + return co(function*() { + try { + let result = yield promiseFunc.apply(null, args); + onResolve(result); + } catch (e) { + if (e && e.uerr) { + onReject(e.uerr.message); + } else { + onReject(e); + } + } + }) + }; } - // Save DB - if (server) { - yield server.disconnect(); + + function connect(callback, useDefaultConf) { + return function () { + var cbArgs = arguments; + var dbName = program.mdb || "duniter_default"; + var dbHome = program.home; + + const home = directory.getHome(dbName, dbHome); + var server = duniter(home, program.memory === true, commandLineConf(program)); + + // If ever the process gets interrupted + let isSaving = false; + closeCommand = () => co(function*() { + if (!isSaving) { + isSaving = true; + // Save DB + return server.disconnect(); + } + }); + + // Initialize server (db connection, ...) + return server.plugFileSystem(useDefaultConf) + .then(() => server.loadConf()) + .then(function () { + try { + cbArgs.length--; + cbArgs[cbArgs.length++] = server; + cbArgs[cbArgs.length++] = server.conf; + return callback.apply(this, cbArgs); + } catch(e) { + server.disconnect(); + throw e; + } + }); + }; } - }); - }))); - -program - .command('gen-next [host] [port] [difficulty]') - .description('Tries to generate the next block of the blockchain') - .action(subCommand(service(generateAndSend((server) => server.BlockchainService.generateNext)))); - -program - .command('gen-root [host] [port] [difficulty]') - .description('Tries to generate root block, with choice of root members') - .action(subCommand(service(function (host, port, difficulty, server, conf) { - if (!host) { - throw 'Host is required.'; + + function service(callback, nologs) { + + return function () { + + if (nologs) { + // Disable logs + require('../app/lib/logger')().mute(); + } + + var cbArgs = arguments; + var dbName = program.mdb; + var dbHome = program.home; + + // Add log files for this instance + logger.addHomeLogs(directory.getHome(dbName, dbHome)); + + const home = directory.getHome(dbName, dbHome); + var server = duniter(home, program.memory === true, commandLineConf(program)); + + // If ever the process gets interrupted + let isSaving = false; + closeCommand = () => co(function*() { + if (!isSaving) { + isSaving = true; + // Save DB + return server.disconnect(); + } + }); + + const that = this; + + // Initialize server (db connection, ...) + return co(function*() { + try { + yield server.initWithDAL(); + yield configure(program, server, server.conf || {}); + yield server.loadConf(); + cbArgs.length--; + cbArgs[cbArgs.length++] = server; + cbArgs[cbArgs.length++] = server.conf; + cbArgs[cbArgs.length++] = program; + onService && onService(server); + return callback.apply(that, cbArgs); + } catch (e) { + server.disconnect(); + throw e; + } + }); + }; } - if (!port) { - throw 'Port is required.'; + + onService = onServiceCallback; + program.parse(programArgs); + + if (programArgs.length <= 2) { + onReject('No command given.'); } - if (!difficulty) { - throw 'Difficulty is required.'; + + const res = yield currentCommand; + if (closeCommand) { + yield closeCommand(); } - return generateAndSend((server) => server.BlockchainService.generateManualRoot)(host, port, difficulty, server, conf); - }))); + return res; + }) + }; +}; + +/**************** + * + * UTILITIES + * + ****************/ -function generateAndSend(getGenerationMethod) { +function generateAndSend(program, getGenerationMethod) { return function (host, port, difficulty, server, conf) { return new Promise((resolve, reject) => { async.waterfall([ @@ -368,7 +578,7 @@ function generateAndSend(getGenerationMethod) { }); }, function (pair, next) { - proveAndSend(server, block, pair.publicKey, parseInt(difficulty), host, parseInt(port), next); + proveAndSend(program, server, block, pair.publicKey, parseInt(difficulty), host, parseInt(port), next); } ], next); } @@ -381,7 +591,7 @@ function generateAndSend(getGenerationMethod) { }; } -function proveAndSend(server, block, issuer, difficulty, host, port, done) { +function proveAndSend(program, server, block, issuer, difficulty, host, port, done) { var BlockchainService = server.BlockchainService; async.waterfall([ function (next) { @@ -414,101 +624,7 @@ function proveAndSend(server, block, issuer, difficulty, host, port, done) { ], done); } -program - .command('export-bc [upto]') - .description('Exports the whole blockchain as JSON array, up to [upto] block number (excluded).') - .action(subCommand(service(function (upto, server) { - return co(function *() { - try { - let CHUNK_SIZE = 500; - let jsoned = []; - let current = yield server.dal.getCurrentBlockOrNull(); - let lastNumber = current ? current.number + 1 : -1; - if (upto !== undefined && upto.match(/\d+/)) { - lastNumber = Math.min(parseInt(upto), lastNumber); - } - let chunksCount = Math.floor(lastNumber / CHUNK_SIZE); - let chunks = []; - // Max-size chunks - for (let i = 0, len = chunksCount; i < len; i++) { - chunks.push({start: i * CHUNK_SIZE, to: i * CHUNK_SIZE + CHUNK_SIZE - 1}); - } - // A last chunk - if (lastNumber > chunksCount * CHUNK_SIZE) { - chunks.push({start: chunksCount * CHUNK_SIZE, to: lastNumber}); - } - for (const chunk of chunks) { - let blocks = yield server.dal.getBlocksBetween(chunk.start, chunk.to); - blocks.forEach(function (block) { - jsoned.push(_(new Block(block).json()).omit('raw')); - }); - } - if (!program.nostdout) { - console.log(JSON.stringify(jsoned, null, " ")); - } - yield server.disconnect(); - return jsoned; - } catch(err) { - logger.warn(err.message || err); - yield server.disconnect(); - } - }); - }, NO_LOGS))); - -program - .command('check-config') - .description('Checks the node\'s configuration') - .action(subCommand(service(function (server) { - return server.checkConfig() - .then(function () { - logger.warn('Configuration seems correct.'); - }) - }))); - -program - .command('config') - .description('Register configuration in database') - .action(subCommand(connect(configure))); - -program - .command('reset [config|data|peers|tx|stats|all]') - .description('Reset configuration, data, peers, transactions or everything in the database') - .action(subCommand((type) => { - let init = ['data', 'all'].indexOf(type) !== -1 ? server : connect; - return init(function (server) { - if (!~['config', 'data', 'peers', 'stats', 'all'].indexOf(type)) { - throw constants.ERRORS.CLI_CALLERR_RESET; - } - return co(function*() { - try { - if (type == 'data') { - yield server.resetData(); - logger.warn('Data successfully reseted.'); - } - if (type == 'peers') { - yield server.resetPeers(); - logger.warn('Peers successfully reseted.'); - } - if (type == 'stats') { - yield server.resetStats(); - logger.warn('Stats successfully reseted.'); - } - if (type == 'config') { - yield server.resetConf(); - logger.warn('Configuration successfully reseted.'); - } - if (type == 'all') { - yield server.resetAll(); - logger.warn('Data & Configuration successfully reseted.'); - } - } catch (e) { - logger.error(e); - } - }); - }, type != 'peers')(type); - })); - -function startWizard(step, server, conf, done) { +function startWizard(service, step, server, conf, done) { var wiz = wizard(server); var task = { 'currency': wiz.configCurrency, @@ -544,7 +660,7 @@ function startWizard(step, server, conf, done) { ], done); } -function commandLineConf(conf) { +function commandLineConf(program, conf) { conf = conf || {}; conf.sync = conf.sync || {}; @@ -656,56 +772,21 @@ function commandLineConf(conf) { return _(conf).extend({routing: true}); } -function connect(callback, useDefaultConf) { - return function () { - var cbArgs = arguments; - var dbName = program.mdb || "duniter_default"; - var dbHome = program.home; - - const home = directory.getHome(dbName, dbHome); - var server = duniter(home, program.memory === true, commandLineConf()); - - // If ever the process gets interrupted - let isSaving = false; - closeCommand = () => co(function*() { - if (!isSaving) { - isSaving = true; - // Save DB - return server.disconnect(); - } - }); - - // Initialize server (db connection, ...) - return server.plugFileSystem(useDefaultConf) - .then(() => server.loadConf()) - .then(function () { - try { - cbArgs.length--; - cbArgs[cbArgs.length++] = server; - cbArgs[cbArgs.length++] = server.conf; - return callback.apply(this, cbArgs); - } catch(e) { - server.disconnect(); - throw e; - } - }); - }; -} - /** * Super basic server with only its home path set + * @param program * @param callback * @param useDefaultConf * @returns {Function} */ -function server(callback, useDefaultConf) { +function server(program, callback, useDefaultConf) { return function () { var cbArgs = arguments; var dbName = program.mdb || "duniter_default"; var dbHome = program.home; const home = directory.getHome(dbName, dbHome); - var server = duniter(home, program.memory === true, commandLineConf()); + var server = duniter(home, program.memory === true, commandLineConf(program)); cbArgs.length--; cbArgs[cbArgs.length++] = server; @@ -714,86 +795,17 @@ function server(callback, useDefaultConf) { }; } -function service(callback, nologs) { - - return function () { - - if (nologs) { - // Disable logs - require('../app/lib/logger')().mute(); - } - - var cbArgs = arguments; - var dbName = program.mdb; - var dbHome = program.home; - - // Add log files for this instance - logger.addHomeLogs(directory.getHome(dbName, dbHome)); - - const home = directory.getHome(dbName, dbHome); - var server = duniter(home, program.memory === true, commandLineConf()); - - // If ever the process gets interrupted - let isSaving = false; - closeCommand = () => co(function*() { - if (!isSaving) { - isSaving = true; - // Save DB - return server.disconnect(); - } - }); - - const that = this; - - // Initialize server (db connection, ...) - return co(function*() { - try { - yield server.initWithDAL(); - yield configure(server, server.conf || {}); - yield server.loadConf(); - cbArgs.length--; - cbArgs[cbArgs.length++] = server; - cbArgs[cbArgs.length++] = server.conf; - cbArgs[cbArgs.length++] = program; - onService && onService(server); - return callback.apply(that, cbArgs); - } catch (e) { - server.disconnect(); - throw e; - } - }); - }; -} - function parsePercent(s) { var f = parseFloat(s); return isNaN(f) ? 0 : f; } -program - .on('*', function (cmd) { - console.log("Unknown command '%s'. Try --help for a listing of commands & options.", cmd); - throw Error("Exiting"); - }); - -module.exports.addCommand = (command, requirements, promiseCallback) => { - program - .command(command.name) - .description(command.desc) - .action(subCommand(service(promiseCallback))); -}; - -module.exports.addOption = (optFormat, optDesc, optParser) => { - program - .option(optFormat, optDesc, optParser); -}; - function needsToBeLaunchedByScript() { logger.error('This command must not be launched directly, using duniter.sh script'); return Promise.resolve(); } -function configure(server, conf) { +function configure(program, server, conf) { return co(function *() { if (typeof server == "string" || typeof conf == "string") { throw constants.ERRORS.CLI_CALLERR_CONFIG; diff --git a/app/lib/dal/fileDAL.js b/app/lib/dal/fileDAL.js index 111ebbc6e282df437f043096382db0d315a2f5d4..c8b7c1f015a6657ece5cf3171c1d31904e3f5c09 100644 --- a/app/lib/dal/fileDAL.js +++ b/app/lib/dal/fileDAL.js @@ -686,10 +686,16 @@ function FileDAL(params) { }); this.saveConf = (confToSave) => { - // TODO: Do something about the currency global variable - currency = confToSave.currency; - // Save the conf in file - return that.confDAL.saveConf(confToSave); + return co(function*() { + // TODO: Do something about the currency global variable + currency = confToSave.currency; + // Save the conf in file + let theConf = confToSave; + if (that.saveConfHook) { + theConf = yield that.saveConfHook(theConf); + } + return that.confDAL.saveConf(theConf); + }); }; /*********************** diff --git a/bin/duniter b/bin/duniter index cc6a71a91ee649d1bace05fd873a9b323c4b4ee3..153524c40864e8aa710a83505ec03f7d031bf916 100755 --- a/bin/duniter +++ b/bin/duniter @@ -5,20 +5,27 @@ const co = require('co'); const duniter = require('../index'); const stack = duniter.statics.autoStack(); -stack.registerDependency({ - duniter: { - cli: [{ - name: 'hello', - desc: 'Says hello to the world.', - requires: ['service'], - promiseCallback: (duniterServer) => co(function*(){ - console.log('Hello, world.'); - }) - }] - } -}); +return co(function*() { -return co(function*(){ - yield stack.executeStack(); - console.log('Done'); + // Specific errors handling + process.on('uncaughtException', (err) => { + // Dunno why this specific exception is not caught + if (err.code !== "EADDRNOTAVAIL" && err.code !== "EINVAL") { + duniter.statics.logger.error(err); + process.exit(2); + } + }); + + try { + yield stack.executeStack(process.argv); + // Everything went well, close Duniter quietly. + process.exit(); + } catch (e) { + // If an unhandled error occured + duniter.statics.logger.error(e); + process.exit(1); + } finally { + // If we did not succeed to close before, force close with error. + process.exit(100); + } }); diff --git a/index.js b/index.js index 76df86eb0afb33a1c4115e0102d2b6c14f9df58c..9eddb7a6de43e73316cfc26770d3f577a787a204 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,30 @@ "use strict"; +const Q = require('q'); const co = require('co'); +const util = require('util'); +const stream = require('stream'); const _ = require('underscore'); const Server = require('./server'); +const directory = require('./app/lib/system/directory'); +const constants = require('./app/lib/constants'); +const wizard = require('./app/lib/wizard'); const logger = require('./app/lib/logger')('duniter'); + +const configDependency = { + duniter: { + cli: [{ + name: 'config', + desc: 'Register configuration in database', + // The command does nothing particular, it just stops the process right after configuration phase is over + onConfiguredExecute: (server, conf, program, params) => Promise.resolve(conf) + }] + } +}; + +const DEFAULT_DEPENDENCIES = [configDependency]; + module.exports = function (home, memory, overConf) { return new Server(home, memory, overConf); }; @@ -13,121 +33,426 @@ module.exports.statics = { logger: logger, - /************** - * Duniter used by its Command Line Interface - * @param onService A callback for external usage when Duniter server is ready + /** + * Creates a new stack with core registrations only. */ - cli: (onService) => { + simpleStack: () => new Stack(DEFAULT_DEPENDENCIES), - const cli = require('./app/cli'); + /** + * Creates a new stack pre-registered with compliant modules found in package.json + */ + autoStack: () => { + const pjson = require('./package.json'); + const duniterModules = []; - // Specific errors handling - process.on('uncaughtException', (err) => { - // Dunno why this specific exception is not caught - if (err.code !== "EADDRNOTAVAIL" && err.code !== "EINVAL") { - logger.error(err); - process.exit(1); + // Look for compliant packages + const prodDeps = Object.keys(pjson.dependencies); + const devDeps = Object.keys(pjson.devDependencies); + const duniterDeps = _.filter(prodDeps.concat(devDeps), (dep) => dep.match(/^duniter-/)); + for(const dep of duniterDeps) { + const required = require(dep); + if (required.duniter) { + duniterModules.push({ + name: dep, + required + }); } - }); + } - process.on('unhandledRejection', (reason) => { - logger.error('Unhandled rejection: ' + reason); - }); + // The dependencies found in package.json + const foundDependencies = duniterModules.map(duniterModule => duniterModule.required); - return co(function*() { - try { - // Prepare the command - const command = cli(process.argv); - // If ever the process gets interrupted - process.on('SIGINT', () => { - co(function*() { - yield command.closeCommand(); - process.exit(); - }); - }); - // Executes the command - yield command.execute(onService); - process.exit(); - } catch (e) { - logger.error(e); - process.exit(1); + // The final stack + return new Stack(DEFAULT_DEPENDENCIES.concat(foundDependencies)); + } +}; + +function Stack(dependencies) { + + const that = this; + const cli = require('./app/cli')(); + const configLoadingCallbacks = []; + const configBeforeSaveCallbacks = []; + const INPUT = new InputStream(); + const PROCESS = new ProcessStream(); + + const streams = { + input: [], + process: [], + output: [], + }; + + this.registerDependency = (requiredObject) => { + const def = requiredObject.duniter; + for (const opt of (def.cliOptions || [])) { + cli.addOption(opt.value, opt.desc, opt.parser); + } + for (const command of (def.cli || [])) { + cli.addCommand({ + name: command.name, + desc: command.desc + }, (...args) => that.processCommand.apply(null, [command].concat(args))); + } + + /** + * Configuration injection + * ----------------------- + */ + if (def.config) { + if (def.config.onLoading) { + configLoadingCallbacks.push(def.config.onLoading); } - }); + // Before the configuration is saved, the module can make some injection/cleaning + if (def.config.beforeSave) { + configBeforeSaveCallbacks.push(def.config.beforeSave); + } + } - }, + /** + * Service injection + * ----------------- + */ + if (def.service) { + // To feed data coming from some I/O (network, disk, other module, ...) + if (def.service.input) { + streams.input.push(def.service.input); + } + // To handle data that has been submitted by INPUT stream + if (def.service.process) { + streams.process.push(def.service.process); + } + // To handle data that has been validated by PROCESS stream + if (def.service.output) { + streams.output.push(def.service.output); + } + } + }; - autoStack: () => { + this.processCommand = (...args) => co(function*() { + const command = args[0]; + const program = args[1]; + const params = args.slice(2); + params.pop(); // Don't need the command argument + + const dbName = program.mdb; + const dbHome = program.home; + const home = directory.getHome(dbName, dbHome); + + // Add log files for this instance + logger.addHomeLogs(home); + + const server = new Server(home, program.memory === true, commandLineConf(program)); + + // If ever the process gets interrupted + let isSaving = false; + process.on('SIGINT', () => { + co(function*() { + if (!isSaving) { + isSaving = true; + // Save DB + return server.disconnect(); + } + process.exit(); + }); + }); - const cli = require('./app/cli'); - const stack = { + // Initialize server (db connection, ...) + try { + yield server.plugFileSystem(); - registerDependency: (requiredObject) => { - for (const opt of (requiredObject.duniter.cliOptions || [])) { - cli.addOption(opt.value, opt.desc, opt.parser); + // Register the configuration hook for loading phase (overrides the loaded data) + server.loadConfHook = (conf) => co(function*() { + // Loading injection + for (const callback of configLoadingCallbacks) { + yield callback(conf, program); } - for (const command of (requiredObject.duniter.cli || [])) { - cli.addCommand({ name: command.name, desc: command.desc }, command.requires, command.promiseCallback); + }); + + // Register the configuration hook for saving phase (overrides the saved data) + server.dal.saveConfHook = (conf) => co(function*() { + const clonedConf = _.clone(conf); + for (const callback of configBeforeSaveCallbacks) { + yield callback(clonedConf, program); } - }, + return clonedConf; + }); - executeStack: () => { + const conf = yield server.loadConf(); + // Auto-configuration default + yield configure(program, server, server.conf || {}); + // Autosave conf + try { + yield server.dal.saveConf(conf); + logger.debug("Configuration saved."); + } catch (e) { + logger.error("Configuration could not be saved: " + e); + throw Error(e); + } + // First possible class of commands: post-config + if (command.onConfiguredExecute) { + return yield command.onConfiguredExecute(server, conf, program, params); + } + // Second possible class of commands: post-service + yield server.initDAL(); + return yield command.onPluggedDALExecute(server, conf, program, params, - // Specific errors handling - process.on('uncaughtException', (err) => { - // Dunno why this specific exception is not caught - if (err.code !== "EADDRNOTAVAIL" && err.code !== "EINVAL") { - logger.error(err); - process.exit(1); + // Start services and streaming between them + () => { + const modules = streams.input.concat(streams.process).concat(streams.output); + for (const module of modules) { + // Any streaming module must implement a `startService` method + module.startService(); } - }); - - process.on('unhandledRejection', (reason) => { - logger.error('Unhandled rejection: ' + reason); - }); + // All inputs write to global INPUT stream + for (const module of streams.input) module.pipe(INPUT); + // All processes read from global INPUT stream + for (const module of streams.process) INPUT.pipe(module); + // All processes write to global PROCESS stream + for (const module of streams.process) module.pipe(PROCESS); + // All ouputs read from global PROCESS stream + for (const module of streams.process) PROCESS.pipe(module); + }, - return co(function*() { - try { - // Prepare the command - const command = cli(process.argv); - // If ever the process gets interrupted - process.on('SIGINT', () => { - co(function*() { - yield command.closeCommand(); - process.exit(); - }); - }); - // Executes the command - yield command.execute(); - process.exit(); - } catch (e) { - logger.error(e); - process.exit(1); + // Stop services and streaming between them + () => { + const modules = streams.input.concat(streams.process).concat(streams.output); + for (const module of modules) { + // Any streaming module must implement a `stopService` method + module.stopService(); } + // Stop reading inputs + for (const module of streams.input) module.unpipe(); + // Stop reading from global INPUT + INPUT.unpipe(); + for (const module of streams.process) module.unpipe(); + // Stop reading from global PROCESS + PROCESS.unpipe(); }); + } catch (e) { + server.disconnect(); + throw e; + } + }); + + this.executeStack = (argv) => { + + // Trace these errors + process.on('unhandledRejection', (reason) => { + logger.error('Unhandled rejection: ' + reason); + }); + + // Executes the command + return cli.execute(argv); + }; + + // We register the initial dependencies right now. Others can be added thereafter. + for (const dep of dependencies) { + that.registerDependency(dep); + } +} + +function commandLineConf(program, conf) { + + conf = conf || {}; + conf.sync = conf.sync || {}; + var cli = { + currency: program.currency, + cpu: program.cpu, + server: { + port: program.port, + ipv4address: program.ipv4, + ipv6address: program.ipv6, + salt: program.salt, + passwd: program.passwd, + remote: { + host: program.remoteh, + ipv4: program.remote4, + ipv6: program.remote6, + port: program.remotep } - }; + }, + db: { + mport: program.mport, + mdb: program.mdb, + home: program.home + }, + net: { + upnp: program.upnp, + noupnp: program.noupnp + }, + logs: { + http: program.httplogs, + nohttp: program.nohttplogs + }, + endpoints: [], + rmEndpoints: [], + ucp: { + rootoffset: program.rootoffset, + sigPeriod: program.sigPeriod, + sigStock: program.sigStock, + sigWindow: program.sigWindow, + idtyWindow: program.idtyWindow, + msWindow: program.msWindow, + sigValidity: program.sigValidity, + sigQty: program.sigQty, + msValidity: program.msValidity, + powZeroMin: program.powZeroMin, + powPeriod: program.powPeriod, + powDelay: program.powDelay, + participate: program.participate, + ud0: program.ud0, + c: program.growth, + dt: program.dt, + incDateMin: program.incDateMin, + medtblocks: program.medtblocks, + dtdiffeval: program.dtdiffeval, + avgGenTime: program.avgGenTime + }, + isolate: program.isolate, + forksize: program.forksize, + nofork: program.nofork, + timeout: program.timeout + }; - const pjson = require('./package.json'); - const duniterModules = []; + // Update conf + if (cli.currency) conf.currency = cli.currency; + if (cli.server.ipv4address) conf.ipv4 = cli.server.ipv4address; + if (cli.server.ipv6address) conf.ipv6 = cli.server.ipv6address; + if (cli.server.port) conf.port = cli.server.port; + if (cli.server.salt) conf.salt = cli.server.salt; + if (cli.server.passwd != undefined) conf.passwd = cli.server.passwd; + if (cli.server.remote.host != undefined) conf.remotehost = cli.server.remote.host; + if (cli.server.remote.ipv4 != undefined) conf.remoteipv4 = cli.server.remote.ipv4; + if (cli.server.remote.ipv6 != undefined) conf.remoteipv6 = cli.server.remote.ipv6; + if (cli.server.remote.port != undefined) conf.remoteport = cli.server.remote.port; + if (cli.ucp.rootoffset) conf.rootoffset = cli.ucp.rootoffset; + if (cli.ucp.sigPeriod) conf.sigPeriod = cli.ucp.sigPeriod; + if (cli.ucp.sigStock) conf.sigStock = cli.ucp.sigStock; + if (cli.ucp.sigWindow) conf.sigWindow = cli.ucp.sigWindow; + if (cli.ucp.idtyWindow) conf.idtyWindow = cli.ucp.idtyWindow; + if (cli.ucp.msWindow) conf.msWindow = cli.ucp.msWindow; + if (cli.ucp.sigValidity) conf.sigValidity = cli.ucp.sigValidity; + if (cli.ucp.msValidity) conf.msValidity = cli.ucp.msValidity; + if (cli.ucp.sigQty) conf.sigQty = cli.ucp.sigQty; + if (cli.ucp.msValidity) conf.msValidity = cli.ucp.msValidity; + if (cli.ucp.powZeroMin) conf.powZeroMin = cli.ucp.powZeroMin; + if (cli.ucp.powPeriod) conf.powPeriod = cli.ucp.powPeriod; + if (cli.ucp.powDelay) conf.powDelay = cli.ucp.powDelay; + if (cli.ucp.participate) conf.participate = cli.ucp.participate == 'Y'; + if (cli.ucp.dt) conf.dt = cli.ucp.dt; + if (cli.ucp.c) conf.c = cli.ucp.c; + if (cli.ucp.ud0) conf.ud0 = cli.ucp.ud0; + if (cli.ucp.incDateMin) conf.incDateMin = cli.ucp.incDateMin; + if (cli.ucp.medtblocks) conf.medianTimeBlocks = cli.ucp.medtblocks; + if (cli.ucp.avgGenTime) conf.avgGenTime = cli.ucp.avgGenTime; + if (cli.ucp.dtdiffeval) conf.dtDiffEval = cli.ucp.dtdiffeval; + if (cli.net.upnp) conf.upnp = true; + if (cli.net.noupnp) conf.upnp = false; + if (cli.cpu) conf.cpu = Math.max(0.01, Math.min(1.0, cli.cpu)); + if (cli.logs.http) conf.httplogs = true; + if (cli.logs.nohttp) conf.httplogs = false; + if (cli.db.mport) conf.mport = cli.db.mport; + if (cli.db.home) conf.home = cli.db.home; + if (cli.db.mdb) conf.mdb = cli.db.mdb; + if (cli.isolate) conf.isolate = cli.isolate; + if (cli.timeout) conf.timeout = cli.timeout; + if (cli.forksize != null) conf.forksize = cli.forksize; - // Look for compliant packages - const prodDeps = Object.keys(pjson.dependencies); - const devDeps = Object.keys(pjson.devDependencies); - const duniterDeps = _.filter(prodDeps.concat(devDeps), (dep) => dep.match(/^duniter-/)); - for(const dep of duniterDeps) { - const required = require(dep); - if (required.duniter) { - duniterModules.push({ - name: dep, - required - }); + // Specific internal settings + conf.createNext = true; + return _(conf).extend({routing: true}); +} + +function configure(program, server, conf) { + return co(function *() { + if (typeof server == "string" || typeof conf == "string") { + throw constants.ERRORS.CLI_CALLERR_CONFIG; + } + let wiz = wizard(); + // UPnP override + if (program.noupnp === true) { + conf.upnp = false; + } + if (program.upnp === true) { + conf.upnp = true; + } + // Network autoconf + const autoconfNet = program.autoconf + || !(conf.ipv4 || conf.ipv6) + || !(conf.remoteipv4 || conf.remoteipv6 || conf.remotehost) + || !(conf.port && conf.remoteport); + if (autoconfNet) { + yield Q.nbind(wiz.networkReconfiguration, wiz)(conf, autoconfNet, program.noupnp); + } + const hasSaltPasswdKey = conf.salt && conf.passwd; + const hasKeyPair = conf.pair && conf.pair.pub && conf.pair.sec; + const autoconfKey = program.autoconf || (!hasSaltPasswdKey && !hasKeyPair); + if (autoconfKey) { + yield Q.nbind(wiz.keyReconfigure, wiz)(conf, autoconfKey); + } + // Try to add an endpoint if provided + if (program.addep) { + if (conf.endpoints.indexOf(program.addep) === -1) { + conf.endpoints.push(program.addep); + } + // Remove it from "to be removed" list + const indexInRemove = conf.rmEndpoints.indexOf(program.addep); + if (indexInRemove !== -1) { + conf.rmEndpoints.splice(indexInRemove, 1); + } + } + // Try to remove an endpoint if provided + if (program.remep) { + if (conf.rmEndpoints.indexOf(program.remep) === -1) { + conf.rmEndpoints.push(program.remep); + } + // Remove it from "to be added" list + const indexInToAdd = conf.endpoints.indexOf(program.remep); + if (indexInToAdd !== -1) { + conf.endpoints.splice(indexInToAdd, 1); } } + }); +} + +/** + * InputStream is a special stream that filters what passes in. + * Only DUP-like documents should be treated by the processing tools, to avoid JSON injection and save CPU cycles. + * @constructor + */ +function InputStream() { + + const that = this; - for (const duniterModule of duniterModules) { - stack.registerDependency(duniterModule.required); + stream.Transform.call(this, { objectMode: true }); + + this._write = function (str, enc, done) { + if (typeof str === 'string') { + // Keep only strings + const matches = str.match(/Type: (.*)\n/); + if (matches && matches[0].match(/(Block|Membership|Identity|Certification|Transaction|Peer)/)) { + const type = matches[0].toLowerCase(); + that.push({ type, doc: str }); + } } + done && done(); + }; +} - return stack; - } -}; +function ProcessStream() { + + const that = this; + + stream.Transform.call(this, { objectMode: true }); + + this._write = function (obj, enc, done) { + // Never close the stream + if (obj !== undefined && obj !== null) { + that.push(obj); + } + done && done(); + }; +} + +util.inherits(InputStream, stream.Transform); +util.inherits(ProcessStream, stream.Transform); diff --git a/server.js b/server.js index 8c7218bb2b510fbcd72bb330f0e0046a9afcab4f..ff30ef2bc452329aa0cec06ccb46ec739f547765 100644 --- a/server.js +++ b/server.js @@ -141,6 +141,9 @@ function Server (home, memoryOnly, overrideConf) { else if (that.conf.passwd || that.conf.salt) { keyPair = yield keyring.scryptKeyPair(that.conf.salt, that.conf.passwd); } + if (that.loadConfHook) { + yield that.loadConfHook(that.conf); + } if (keyPair) { that.keyPair = keyPair; that.sign = keyPair.sign; diff --git a/test/fast/v1.0-modules-api.js b/test/fast/v1.0-modules-api.js new file mode 100644 index 0000000000000000000000000000000000000000..130b60b07888931b91dacbc94b4617137d6e2da6 --- /dev/null +++ b/test/fast/v1.0-modules-api.js @@ -0,0 +1,132 @@ +"use strict"; + +const co = require('co'); +const _ = require('underscore'); +const should = require('should'); +const duniter = require('../../index'); + +describe("v1.0 Module API", () => { + + it('should be able to execute `hello` command', () => co(function*() { + + const sStack = duniter.statics.simpleStack(); + const aStack = duniter.statics.autoStack(); + + const helloDependency = { + duniter: { + cliOptions: [ + { value: '--opt1', desc: 'The option 1. Enabled or not' }, + { value: '--option2 <value>', desc: 'The option 2. Requires an argument, parsed as integer.', parser: parseInt } + ], + cli: [{ + name: 'hello', + desc: 'Returns an "Hello, world" string after configuration phase.', + onConfiguredExecute: (server, conf, program, params) => co(function*(){ + return "Hello, " + params[0] + ". You successfully sent arg '" + params[1] + "' along with opt1 = " + program.opt1 + " and option2 = " + program.option2 + "."; + }) + }] + } + }; + + sStack.registerDependency(helloDependency); + aStack.registerDependency(helloDependency); + + (yield sStack.executeStack(['node', 'index.js', 'hello', 'World', 'TEST', '--opt1', '--option2', '5'])).should.equal('Hello, World. You successfully sent arg \'TEST\' along with opt1 = true and option2 = 5.'); + (yield aStack.executeStack(['node', 'index.js', 'hello', 'Zorld', 'ESSE', '--option2', 'd'])).should.equal('Hello, Zorld. You successfully sent arg \'ESSE\' along with opt1 = undefined and option2 = NaN.'); + })); + + /*********************** + * CONFIGURATION HOOKS + **********************/ + + describe("Configuration hooks", () => { + + let stack; + const run = (...args) => stack.executeStack(['node', 'index.js', '--mdb', 'modules_api_tests'].concat(args)); + + before(() => co(function*() { + + stack = duniter.statics.simpleStack(); + const configurationDependency = { + duniter: { + cliOptions: [ + { value: '--supersalt <salt>', desc: 'A crypto salt.' }, + { value: '--superpasswd <passwd>', desc: 'A crypto password.' } + ], + config: { + onLoading: (conf, program) => co(function*(){ + + // Always adds a parameter named "superkey" + conf.superkey = { pub: 'publicPart', sec: 'secretPart' }; + // Eventually adds a supersalt if given as option + if (program.supersalt) { + conf.supersalt = program.supersalt; + } + // Eventually adds a superpasswd if given as option + if (program.superpasswd) { + conf.superpasswd = program.superpasswd; + } + }), + beforeSave: (conf, program) => co(function*(){ + + // We never want to store "superpasswd" + delete conf.superpasswd; + }) + } + } + }; + const returnConfDependency = { + duniter: { + cli: [{ + name: 'gimme-conf', + desc: 'Returns the configuration object.', + onPluggedDALExecute: (server, conf, program, params, startServices, stopServices) => co(function*() { + // Gimme the conf! + return conf; + }) + }], + } + }; + + stack.registerDependency(configurationDependency); + stack.registerDependency(returnConfDependency); + })); + + it('verify that we get the CLI options', () => co(function*() { + const conf = yield run('gimme-conf', '--supersalt', 'NaCl'); + conf.should.have.property('supersalt').equal('NaCl'); + })); + + it('verify that we get the saved options', () => co(function*() { + let conf; + + // We make an initial reset + yield run('reset', 'config'); + conf = yield run('gimme-conf'); + conf.should.have.property('superkey'); // Always loaded + conf.should.not.have.property('supersalt'); + + // Nothing should have changed + conf = yield run('gimme-conf'); + conf.should.have.property('superkey'); // Always loaded + conf.should.not.have.property('supersalt'); + + // Now we try to save the parameters + yield run('config', '--supersalt', 'NaCl2', '--superpasswd', 'megapasswd'); + conf = yield run('gimme-conf'); + conf.should.have.property('superkey'); // Always loaded + conf.should.have.property('supersalt').equal('NaCl2'); + conf.should.not.have.property('superpasswd'); + + // Yet we can have all options by giving them explicitely using options + conf = yield run('gimme-conf', '--superpasswd', 'megapasswd2'); + conf.should.have.property('superkey'); + conf.should.have.property('supersalt').equal('NaCl2'); + conf.should.have.property('superpasswd').equal('megapasswd2'); + })); + }); + + // TODO: test serviceStart + // TODO: test serviceStop + // TODO: test streaming +}); diff --git a/test/integration/cli.js b/test/integration/cli.js index 1c5ce1a1f478a1831522203a6822d8093b04e448..3935e242b18451556b2a7bb8dfe6a9319fa1cf5f 100644 --- a/test/integration/cli.js +++ b/test/integration/cli.js @@ -6,7 +6,7 @@ const co = require('co'); const should = require('should'); const _ = require('underscore'); const toolbox = require('./tools/toolbox'); -const cli = require('../../app/cli'); +const duniter = require('../../index'); const merkleh = require('../../app/lib/helpers/merkle'); const hashf = require('../../app/lib/ucp/hashf'); const constants = require('../../app/lib/constants'); @@ -161,9 +161,10 @@ describe("CLI", function() { function execute(args) { const finalArgs = [process.argv[0], __filename].concat(args).concat(['--mdb', DB_NAME]); return co(function*() { - const command = cli(finalArgs); + + const stack = duniter.statics.autoStack(); // Executes the command - return command.execute(); + return stack.executeStack(finalArgs); }); }