From 16d5e7b79e8bc7de40ea9b6977a162852c338d0e Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Wed, 25 Sep 2019 17:35:45 +0200 Subject: [PATCH] feat: allow daemon to init and start in a single cmd (#2428) `ipfs daemon` will now init the repo automatically if one does not exist --- .travis.yml | 44 ++++++++++++++--- package.json | 15 +++--- src/cli/commands/daemon.js | 19 +++++++ src/cli/commands/init.js | 22 +++++++-- src/cli/daemon.js | 2 +- test/cli/daemon.js | 68 +++++++++++++++++++++++--- test/cli/init.js | 20 ++++---- test/core/bitswap.spec.js | 2 +- test/core/gc.spec.js | 3 +- test/core/interface.spec.js | 46 +---------------- test/core/name.spec.js | 2 +- test/core/ping.spec.js | 1 - test/http-api/interface.js | 46 +---------------- test/utils/interface-common-factory.js | 58 +++++++++++++++++++++- test/utils/ipfs-exec.js | 1 + 15 files changed, 218 insertions(+), 131 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06d238e928..004c87c771 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,26 +40,58 @@ jobs: addons: chrome: stable script: - - npx aegir test -t browser - - npx aegir test -t webworker + - npx aegir test -t browser -t webworker - stage: test name: firefox addons: firefox: latest script: - - npx aegir test -t browser -- --browsers FirefoxHeadless - - npx aegir test -t webworker -- --browsers FirefoxHeadless + - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - stage: test name: electron-main + os: osx script: - - xvfb-run npx aegir test -t electron-main -- --bail --timeout 10000 + - npx aegir test -t electron-main --bail --timeout 10000 - stage: test name: electron-renderer + os: osx script: - - xvfb-run npx aegir test -t electron-renderer -- --bail --timeout 10000 + - npx aegir test -t electron-renderer --bail --timeout 10000 + + - stage: test + name: interop node + script: + - cd node_modules/ipfs-interop + - IPFS_JS_EXEC=./../../src/cli/bin.js IPFS_REUSEPORT=false npx aegir test -t node --bail + + - stage: test + name: interop browser + script: + - cd node_modules/ipfs-interop + - IPFS_JS_EXEC=./../../src/cli/bin.js IPFS_REUSEPORT=false npx aegir test -t browser --bail + + - stage: test + name: interop electron-main + os: osx + script: + - cd node_modules/ipfs-interop + - IPFS_JS_EXEC=./../../src/cli/bin.js IPFS_REUSEPORT=false npx aegir test -t electron-main -f ./test/node.js --bail --timeout 10000 + + - stage: test + name: interop electron-renderer + os: osx + script: + - cd node_modules/ipfs-interop + - IPFS_JS_EXEC=./../../src/cli/bin.js IPFS_REUSEPORT=false npx aegir test -t electron-renderer -f ./test/browser.js --bail --timeout 10000 + + - stage: tag + if: branch = master AND type = push AND fork = false + name: update-last-successful-build + script: + - npx aegir update-last-successful-build - stage: test name: interop diff --git a/package.json b/package.json index 92dab96b08..67b73f7ca2 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "ipfs-bitswap": "~0.25.1", "ipfs-block": "~0.8.1", "ipfs-block-service": "~0.15.2", - "ipfs-http-client": "^35.1.0", + "ipfs-http-client": "^37.0.2", "ipfs-http-response": "~0.3.1", "ipfs-mfs": "^0.12.2", "ipfs-multipart": "^0.2.0", @@ -105,7 +105,7 @@ "ipfs-unixfs": "~0.1.16", "ipfs-unixfs-exporter": "~0.37.7", "ipfs-unixfs-importer": "~0.39.11", - "ipfs-utils": "~0.2.0", + "ipfs-utils": "~0.4.0", "ipld": "~0.24.1", "ipld-bitcoin": "~0.3.0", "ipld-dag-cbor": "~0.15.0", @@ -124,7 +124,7 @@ "it-to-stream": "^0.1.1", "just-safe-set": "^2.1.0", "kind-of": "^6.0.2", - "ky": "~0.13.0", + "ky": "~0.14.0", "ky-universal": "~0.3.0", "libp2p": "~0.26.1", "libp2p-bootstrap": "~0.9.3", @@ -188,7 +188,7 @@ "yargs-promise": "^1.1.0" }, "devDependencies": { - "aegir": "^20.1.0", + "aegir": "^20.3.1", "base64url": "^3.0.1", "chai": "^4.2.0", "clear-module": "^4.0.0", @@ -199,16 +199,17 @@ "execa": "^2.0.4", "form-data": "^2.5.1", "hat": "0.0.3", - "interface-ipfs-core": "^0.113.0", + "interface-ipfs-core": "~0.114.0", "ipfs-interop": "~0.1.0", - "ipfsd-ctl": "~0.46.0", + "ipfsd-ctl": "^0.47.2", "libp2p-websocket-star": "~0.10.2", "ncp": "^2.0.0", "p-event": "^4.1.0", "qs": "^6.5.2", "rimraf": "^3.0.0", "sinon": "^7.4.2", - "stream-to-promise": "^2.2.0" + "stream-to-promise": "^2.2.0", + "temp-write": "^4.0.0" }, "optionalDependencies": { "prom-client": "^11.5.3", diff --git a/src/cli/commands/daemon.js b/src/cli/commands/daemon.js index cfe48d44d1..6ca2193cf6 100644 --- a/src/cli/commands/daemon.js +++ b/src/cli/commands/daemon.js @@ -1,9 +1,11 @@ 'use strict' const os = require('os') +const fs = require('fs') const toUri = require('multiaddr-to-uri') const { ipfsPathHelp } = require('../utils') const { isTest } = require('ipfs-utils/src/env') +const debug = require('debug')('ipfs:cli:daemon') module.exports = { command: 'daemon', @@ -13,6 +15,10 @@ module.exports = { builder (yargs) { return yargs .epilog(ipfsPathHelp) + .option('init-config', { + type: 'string', + desc: 'Path to existing configuration file to be loaded during --init.' + }) .option('enable-sharding-experiment', { type: 'boolean', default: false @@ -42,9 +48,22 @@ module.exports = { const repoPath = argv.getRepoPath() + let config = {} + // read and parse config file + if (argv.initConfig) { + try { + const raw = fs.readFileSync(argv.initConfig) + config = JSON.parse(raw) + } catch (error) { + debug(error) + throw new Error('Default config couldn\'t be found or content isn\'t valid JSON.') + } + } + // Required inline to reduce startup time const Daemon = require('../../cli/daemon') const daemon = new Daemon({ + config, silent: argv.silent, repo: process.env.IPFS_PATH, offline: argv.offline, diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index 5b41f8afe3..0a3c5de095 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -1,15 +1,17 @@ 'use strict' +const fs = require('fs') +const debug = require('debug')('ipfs:cli:init') const { ipfsPathHelp } = require('../utils') module.exports = { - command: 'init [config] [options]', + command: 'init [default-config] [options]', describe: 'Initialize a local IPFS node', builder (yargs) { return yargs .epilog(ipfsPathHelp) - .positional('config', { - describe: 'Node config, this should JSON and will be merged with the default config. Check https://github.com/ipfs/js-ipfs#optionsconfig', + .positional('default-config', { + describe: 'Initialize with the given configuration. Path to the config file. Check https://github.com/ipfs/js-ipfs#optionsconfig', type: 'string' }) .option('bits', { @@ -34,6 +36,18 @@ module.exports = { argv.resolve((async () => { const path = argv.getRepoPath() + let config = {} + // read and parse config file + if (argv.defaultConfig) { + try { + const raw = fs.readFileSync(argv.defaultConfig) + config = JSON.parse(raw) + } catch (error) { + debug(error) + throw new Error('Default config couldn\'t be found or content isn\'t valid JSON.') + } + } + argv.print(`initializing ipfs node at ${path}`) // Required inline to reduce startup time @@ -44,7 +58,7 @@ module.exports = { repo: new Repo(path), init: false, start: false, - config: argv.config || {} + config }) try { diff --git a/src/cli/daemon.js b/src/cli/daemon.js index 7181e8c1e7..2528a06bc9 100644 --- a/src/cli/daemon.js +++ b/src/cli/daemon.js @@ -55,7 +55,7 @@ class Daemon { } // start the daemon - const ipfsOpts = Object.assign({ init: false }, this._options, { start: true, libp2p }) + const ipfsOpts = Object.assign({ }, this._options, { init: true, start: true, libp2p }) const ipfs = new IPFS(ipfsOpts) await new Promise((resolve, reject) => { diff --git a/test/cli/daemon.js b/test/cli/daemon.js index eb05d981aa..1ec3379fc9 100644 --- a/test/cli/daemon.js +++ b/test/cli/daemon.js @@ -9,10 +9,35 @@ const os = require('os') const path = require('path') const hat = require('hat') const fs = require('fs') +const tempWrite = require('temp-write') const pkg = require('../../package.json') const skipOnWindows = isWindows() ? it.skip : it +const daemonReady = (daemon, cb) => { + let r = null + const p = new Promise((resolve, reject) => { + daemon.stdout.on('data', async (data) => { + if (data.toString().includes('Daemon is ready')) { + try { + r = await cb() + } catch (err) { + reject(err) + } + daemon.cancel() + } + }) + daemon.stderr.on('data', () => reject(new Error('Daemon didnt start'))) + daemon.then(() => resolve(r)).catch(err => { + if (r && err.killed) { + return resolve(r) + } + + reject(err) + }) + }) + return p +} const checkLock = (repo) => { // skip on windows // https://github.com/ipfs/js-ipfsd-ctl/pull/155#issuecomment-326983530 @@ -190,14 +215,6 @@ describe('daemon', () => { checkLock(repoPath) }) - it('gives error if user hasn\'t run init before', async function () { - this.timeout(100 * 1000) - - const err = await ipfs.fail('daemon') - - expect(err.stdout).to.include('no initialized ipfs repo found in ' + repoPath) - }) - it('should be silent', async function () { this.timeout(100 * 1000) await ipfs('init') @@ -265,4 +282,39 @@ describe('daemon', () => { expect(err.stdout).to.include(`Node.js version: ${process.versions.node}`) } }) + + it('should init by default', async function () { + this.timeout(100 * 1000) + + expect(fs.existsSync(repoPath)).to.be.false() + + const daemon = ipfs('daemon') + let stdout = '' + + daemon.stdout.on('data', (data) => { + stdout += data.toString('utf8') + + if (stdout.includes('Daemon is ready')) { + daemon.kill() + } + }) + + try { + await daemon + throw new Error('Did not kill process') + } catch (err) { + expect(err.killed).to.be.true() + } + + expect(fs.existsSync(repoPath)).to.be.true() + }) + + it('should init with custom config', async function () { + this.timeout(100 * 1000) + const configPath = tempWrite.sync('{"Addresses": {"API": "/ip4/127.0.0.1/tcp/9999"}}', 'config.json') + const daemon = ipfs(`daemon --init-config ${configPath}`) + + const r = await daemonReady(daemon, () => ipfs('config \'Addresses.API\'')) + expect(r).to.be.eq('/ip4/127.0.0.1/tcp/9999\n') + }) }) diff --git a/test/cli/init.js b/test/cli/init.js index ab9082a731..e82c78b3f6 100644 --- a/test/cli/init.js +++ b/test/cli/init.js @@ -8,9 +8,10 @@ const clean = require('../utils/clean') const hat = require('hat') const ipfsExec = require('../utils/ipfs-exec') const os = require('os') +const tempWrite = require('temp-write') describe('init', function () { - this.timeout(40 * 1000) + this.timeout(100 * 1000) let repoPath let ipfs @@ -33,8 +34,6 @@ describe('init', function () { afterEach(() => clean(repoPath)) it('basic', function () { - this.timeout(40 * 1000) - return ipfs('init').then((out) => { expect(repoDirSync('blocks')).to.have.length.above(2) expect(repoExistsSync('config')).to.equal(true) @@ -48,8 +47,6 @@ describe('init', function () { }) it('bits', function () { - this.timeout(40 * 1000) - return ipfs('init --bits 1024').then(() => { expect(repoDirSync('blocks')).to.have.length.above(2) expect(repoExistsSync('config')).to.equal(true) @@ -58,8 +55,6 @@ describe('init', function () { }) it('empty', function () { - this.timeout(40 * 1000) - return ipfs('init --bits 1024 --empty-repo true').then(() => { expect(repoDirSync('blocks')).to.have.length(2) expect(repoExistsSync('config')).to.equal(true) @@ -68,11 +63,18 @@ describe('init', function () { }) it('should present ipfs path help when option help is received', function (done) { - this.timeout(100 * 1000) - ipfs('init --help').then((res) => { expect(res).to.have.string('export IPFS_PATH=/path/to/ipfsrepo') done() }) }) + + it('default config argument', () => { + const configPath = tempWrite.sync('{"Addresses": {"API": "/ip4/127.0.0.1/tcp/9999"}}', 'config.json') + return ipfs(`init ${configPath}`).then((res) => { + const configRaw = fs.readFileSync(path.join(repoPath, 'config')).toString() + const config = JSON.parse(configRaw) + expect(config.Addresses.API).to.be.eq('/ip4/127.0.0.1/tcp/9999') + }) + }) }) diff --git a/test/core/bitswap.spec.js b/test/core/bitswap.spec.js index 68506b29c6..df98ffb8df 100644 --- a/test/core/bitswap.spec.js +++ b/test/core/bitswap.spec.js @@ -13,7 +13,7 @@ const waterfall = require('async/waterfall') const parallel = require('async/parallel') const Block = require('ipfs-block') const multiaddr = require('multiaddr') -const isNode = require('detect-node') +const { isNode } = require('ipfs-utils/src/env') const multihashing = require('multihashing-async') const CID = require('cids') const path = require('path') diff --git a/test/core/gc.spec.js b/test/core/gc.spec.js index ab3e148a7c..3fc007d30a 100644 --- a/test/core/gc.spec.js +++ b/test/core/gc.spec.js @@ -35,6 +35,7 @@ class MutexEmitter extends Mutex { } describe('gc', function () { + this.timeout(40 * 1000) const fixtures = [{ path: 'test/my/path1', content: Buffer.from('path1') @@ -54,8 +55,6 @@ describe('gc', function () { let lockEmitter before(async function () { - this.timeout(40 * 1000) - const factory = IPFSFactory.create({ type: 'proc', exec: IPFS }) const config = { Bootstrap: [] } diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index 10e5c225bd..84a386e353 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -4,7 +4,6 @@ const tests = require('interface-ipfs-core') const CommonFactory = require('../utils/interface-common-factory') const isNode = require('detect-node') -const callbackify = require('callbackify') describe('interface-ipfs-core tests', function () { this.timeout(20 * 1000) @@ -150,48 +149,5 @@ describe('interface-ipfs-core tests', function () { tests.stats(defaultCommonFactory) - tests.swarm(CommonFactory.create({ - createSetup ({ ipfsFactory, nodes }) { - const callbackifiedSpawn = callbackify.variadic( - ipfsFactory.spawn.bind(ipfsFactory)) - return callback => { - callback(null, { - spawnNode (repoPath, config, cb) { - if (typeof repoPath === 'function') { - cb = repoPath - repoPath = undefined - } - - if (typeof config === 'function') { - cb = config - config = null - } - - config = config || { - Bootstrap: [], - Discovery: { - MDNS: { - Enabled: false - }, - webRTCStar: { - Enabled: false - } - } - } - - const spawnOptions = { repoPath, config, initOptions: { bits: 512 } } - - callbackifiedSpawn(spawnOptions, (err, _ipfsd) => { - if (err) { - return cb(err) - } - - nodes.push(_ipfsd) - cb(null, _ipfsd.api) - }) - } - }) - } - } - }), { skip: !isNode }) + tests.swarm(CommonFactory.createAsync(), { skip: !isNode }) }) diff --git a/test/core/name.spec.js b/test/core/name.spec.js index c17e2577d8..edef66c500 100644 --- a/test/core/name.spec.js +++ b/test/core/name.spec.js @@ -39,11 +39,11 @@ const publishAndResolve = (publisher, resolver, ipfsRef, publishOpts, nodeId, re describe('name', function () { describe('republisher', function () { + this.timeout(40 * 1000) let node let ipfsd before(async function () { - this.timeout(40 * 1000) ipfsd = await df.spawn({ exec: IPFS, args: [`--pass ${hat()}`, '--offline'], diff --git a/test/core/ping.spec.js b/test/core/ping.spec.js index 9de8966f4a..f05b97d556 100644 --- a/test/core/ping.spec.js +++ b/test/core/ping.spec.js @@ -36,7 +36,6 @@ const spawnNode = ({ dht = false, type = 'js' }) => { return factory.spawn({ args, config, - initOptions: { bits: 512 }, preload: { enabled: false } }) } diff --git a/test/http-api/interface.js b/test/http-api/interface.js index 8c4811822c..3373f36069 100644 --- a/test/http-api/interface.js +++ b/test/http-api/interface.js @@ -4,7 +4,6 @@ const tests = require('interface-ipfs-core') const CommonFactory = require('../utils/interface-common-factory') const path = require('path') -const callbackify = require('callbackify') describe('interface-ipfs-core over ipfs-http-client tests', () => { const defaultCommonFactory = CommonFactory.create({ @@ -144,48 +143,5 @@ describe('interface-ipfs-core over ipfs-http-client tests', () => { tests.stats(defaultCommonFactory) - tests.swarm(CommonFactory.create({ - createSetup ({ ipfsFactory, nodes }) { - const callbackifiedSpawn = callbackify.variadic( - ipfsFactory.spawn.bind(ipfsFactory)) - return callback => { - callback(null, { - spawnNode (repoPath, config, cb) { - if (typeof repoPath === 'function') { - cb = repoPath - repoPath = undefined - } - - if (typeof config === 'function') { - cb = config - config = undefined - } - - config = config || { - Bootstrap: [], - Discovery: { - MDNS: { - Enabled: false - }, - webRTCStar: { - Enabled: false - } - } - } - - const spawnOptions = { repoPath, config, initOptions: { bits: 512 } } - - callbackifiedSpawn(spawnOptions, (err, _ipfsd) => { - if (err) { - return cb(err) - } - - nodes.push(_ipfsd) - cb(null, _ipfsd.api) - }) - } - }) - } - } - })) + tests.swarm(CommonFactory.createAsync()) }) diff --git a/test/utils/interface-common-factory.js b/test/utils/interface-common-factory.js index b6f0e72245..0b2d55fe75 100644 --- a/test/utils/interface-common-factory.js +++ b/test/utils/interface-common-factory.js @@ -69,4 +69,60 @@ function createFactory (options) { } } -exports.create = createFactory +function createAsync (createFactoryOptions = {}, createSpawnOptions = {}) { + return () => { + const nodes = [] + const setup = async (factoryOptions = {}, spawnOptions = {}) => { + factoryOptions = mergeOptions( + { + type: 'proc', + exec: IPFS + }, + factoryOptions, + createFactoryOptions + ) + // When not an in proc daemon use the http-client js-ipfs depends on, not the one from ipfsd-ctl + if (factoryOptions.type !== 'proc') { + factoryOptions.IpfsClient = factoryOptions.IpfsClient || ipfsClient + } + + const ipfsFactory = IPFSFactory.create(factoryOptions) + const node = await ipfsFactory.spawn(mergeOptions( + { + config: { + Bootstrap: [], + Discovery: { + MDNS: { + Enabled: false + }, + webRTCStar: { + Enabled: false + } + } + }, + preload: { enabled: false } + }, + spawnOptions, + createSpawnOptions + )) + nodes.push(node) + + const id = await node.api.id() + node.api.peerId = id + + return node.api + } + + const teardown = () => { + return Promise.all(nodes.map(n => n.stop())) + } + return { + setup, + teardown + } + } +} +module.exports = { + createAsync, + create: createFactory +} diff --git a/test/utils/ipfs-exec.js b/test/utils/ipfs-exec.js index 98253fb6a6..94630a18e4 100644 --- a/test/utils/ipfs-exec.js +++ b/test/utils/ipfs-exec.js @@ -45,6 +45,7 @@ module.exports = (repoPath, opts) => { return res.stdout }) + res.cancel = cp.cancel.bind(cp) res.kill = cp.kill.bind(cp) res.stdin = cp.stdin res.stdout = cp.stdout