From 68535e81c3c964273efef20e6df6a459446f290d Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 29 Aug 2022 19:08:42 +0100 Subject: [PATCH] feat: jsdoc and improvements to daemon logic (#2227) * feat: jsdoc and improvements to daemon logic * fix: only check for config iff api file not exists * fix: re-add Object.freeze to STATUS * docs: clear comment --- assets/locales/en.json | 14 +- src/daemon/config.js | 310 +++++++++++++++++++-------------- src/daemon/consts.js | 24 ++- src/daemon/daemon.js | 137 +++++++++------ src/daemon/dialogs.js | 155 +++++++++++++++++ src/daemon/index.js | 3 +- src/daemon/migration-prompt.js | 18 +- 7 files changed, 461 insertions(+), 200 deletions(-) create mode 100644 src/daemon/dialogs.js diff --git a/assets/locales/en.json b/assets/locales/en.json index 14dbf477e..ad78e5e0f 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -207,12 +207,20 @@ "title": "IPFS Desktop Startup Has Failed", "message": "IPFS node has encountered an error and startup could not be completed:" }, - "invalidRepositoryDialog": { - "title": "Invalid IPFS Repository or Configuration File", - "message": "The repository at “{ path }” is invalid. The “config” file must be a valid JSON.\n\nBefore starting IPFS Desktop again, please fix the configuration file or rename the old repository to “.ipfs.backup”." + "repositoryMustBeDirectoryDialog": { + "title": "IPFS Repository Must Be a Directory", + "message": "The repository at “{ path }” is invalid, as it is not a directory.\n\nBefore starting IPFS Desktop again, please rename the old repository to “.ipfs.backup”. Please note that this will make IPFS Desktop generate a new repository." + }, + "repositoryConfigurationIsMissingDialog": { + "title": "IPFS Repository Configuration Is Missing", + "message": "The repository at “{ path }” does not have a “config” file. The “config” file must be a valid JSON file.\n\nBefore starting IPFS Desktop again, please fix the configuration file or rename the old repository to “.ipfs.backup”. Please note that by renaming the old repository, IPFS Desktop will generate a new repository." }, "privateNetworkDialog": { "title": "Private Network IPFS Repository", "message": "The repository at “{ path }” is part of a private network, which is not supported by IPFS Desktop." + }, + "invalidRepositoryDialog": { + "title": "Invalid IPFS Repository or Configuration File", + "message": "The repository at “{ path }” or its configuration is invalid. The “config” file must be a valid JSON.\n\nBefore starting IPFS Desktop again, please fix the configuration file or rename the old repository to “.ipfs.backup”. Please note that by renaming the old repository, IPFS Desktop will generate a new repository" } } diff --git a/src/daemon/config.js b/src/daemon/config.js index 0c3446067..02b368d7e 100644 --- a/src/daemon/config.js +++ b/src/daemon/config.js @@ -1,42 +1,90 @@ -const { app, BrowserWindow } = require('electron') const { join } = require('path') const fs = require('fs-extra') const { multiaddr } = require('multiaddr') const http = require('http') const portfinder = require('portfinder') const { shell } = require('electron') -const i18n = require('i18next') -const { showDialog } = require('../dialogs') const store = require('../common/store') const logger = require('../common/logger') +const dialogs = require('./dialogs') + +/** + * Get repository configuration file path. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {string} config file path + */ +function getConfigFilePath (ipfsd) { + return join(ipfsd.path, 'config') +} -function configExists (ipfsd) { - return fs.pathExistsSync(join(ipfsd.path, 'config')) +/** + * Get repository api file path. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {string} api file path + */ +function getApiFilePath (ipfsd) { + return join(ipfsd.path, 'api') } -function apiFileExists (ipfsd) { - return fs.pathExistsSync(join(ipfsd.path, 'api')) +/** + * Checks if the repository configuration file exists. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {boolean} true if config file exists + */ +function configExists (ipfsd) { + return fs.pathExistsSync(getConfigFilePath(ipfsd)) } -function rmApiFile (ipfsd) { - return fs.removeSync(join(ipfsd.path, 'api')) +/** + * Checks if the repository api file exists. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {boolean} true if config file exists + */ +function apiFileExists (ipfsd) { + return fs.pathExistsSync(getApiFilePath(ipfsd)) } -function configPath (ipfsd) { - return join(ipfsd.path, 'config') +/** + * Removes the repository api file. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {void} + */ +function removeApiFile (ipfsd) { + fs.removeSync(getApiFilePath(ipfsd)) } +/** + * Reads the repository configuration file. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {any} the configuration + */ function readConfigFile (ipfsd) { - return fs.readJsonSync(configPath(ipfsd)) + return fs.readJsonSync(getConfigFilePath(ipfsd)) } +/** + * Writes the repository configuration file. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @param {Object} config + */ function writeConfigFile (ipfsd, config) { - fs.writeJsonSync(configPath(ipfsd), config, { spaces: 2 }) + fs.writeJsonSync(getConfigFilePath(ipfsd), config, { spaces: 2 }) } -// Set default minimum and maximum of connections to maintain -// by default. This must only be called for repositories created -// by IPFS Desktop. Existing ones shall remain intact. +/** + * Set default minimum and maximum of connections to maintain + * by default. This must only be called for repositories created + * by IPFS Desktop. Existing ones shall remain intact. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + */ function applyDefaults (ipfsd) { const config = readConfigFile(ipfsd) @@ -44,21 +92,38 @@ function applyDefaults (ipfsd) { // See: https://github.com/ipfs/js-ipfsd-ctl/issues/333 config.API = { HTTPHeaders: {} } - config.Swarm = config.Swarm || {} + config.Swarm = config.Swarm ?? {} config.Swarm.DisableNatPortMap = false - config.Swarm.ConnMgr = config.Swarm.ConnMgr || {} + config.Swarm.ConnMgr = config.Swarm.ConnMgr ?? {} config.Swarm.ConnMgr.GracePeriod = '1m' config.Swarm.ConnMgr.LowWater = 20 config.Swarm.ConnMgr.HighWater = 40 - config.Discovery = config.Discovery || {} - config.Discovery.MDNS = config.Discovery.MDNS || {} + config.Discovery = config.Discovery ?? {} + config.Discovery.MDNS = config.Discovery.MDNS ?? {} config.Discovery.MDNS.Enabled = true writeConfigFile(ipfsd, config) } -const getGatewayPort = (config) => getHttpPort(config.Addresses.Gateway) +/** + * Parses multiaddr from the configuration. + * + * @param {string} addr + * @returns {import('multiaddr').Multiaddr} + */ +function parseMultiaddr (addr) { + return addr.includes('/http') + ? multiaddr(addr) + : multiaddr(addr).encapsulate('/http') +} + +/** + * Get local HTTP port. + * + * @param {array|string} addrs + * @returns {number} the port + */ function getHttpPort (addrs) { let httpUrl = null @@ -68,12 +133,24 @@ function getHttpPort (addrs) { httpUrl = addrs } - const gw = parseCfgMultiaddr(httpUrl) + const gw = parseMultiaddr(httpUrl) return gw.nodeAddress().port } -// Apply one-time updates to the config of IPFS node. -// This is the place where we execute fixes and performance tweaks for existing users. +/** + * Get gateway port from configuration. + * + * @param {any} config + * @returns {number} + */ +const getGatewayPort = (config) => getHttpPort(config.Addresses.Gateway) + +/** + * Apply one-time updates to the config of IPFS node. This is the place + * where we execute fixes and performance tweaks for existing users. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + */ function migrateConfig (ipfsd) { // Bump revision number when new migration rule is added const REVISION = 4 @@ -156,11 +233,12 @@ function migrateConfig (ipfsd) { store.set(REVISION_KEY, REVISION) } -const parseCfgMultiaddr = (addr) => (addr.includes('/http') - ? multiaddr(addr) - : multiaddr(addr).encapsulate('/http') -) - +/** + * Checks if the given address is a daemon address. + * + * @param {{ family: 4 | 6, address: string, port: number }} addr + * @returns {Promise} + */ async function checkIfAddrIsDaemon (addr) { const options = { timeout: 3000, // 3s is plenty for localhost request @@ -183,17 +261,30 @@ async function checkIfAddrIsDaemon (addr) { }) } +/** + * Find free close to port. + * + * @param {number} port + * @returns {Promise} + */ const findFreePort = async (port) => { port = Math.max(port, 1024) return portfinder.getPortPromise({ port }) } +/** + * Check if all the ports in the array are available. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @param {string[]} addrs + * @returns {Promise} + */ async function checkPortsArray (ipfsd, addrs) { addrs = addrs.filter(Boolean) for (const addr of addrs) { - const ma = parseCfgMultiaddr(addr) - const port = parseInt(ma.nodeAddress().port, 10) + const ma = parseMultiaddr(addr) + const port = ma.nodeAddress().port if (port === 0) { continue @@ -208,25 +299,25 @@ async function checkPortsArray (ipfsd, addrs) { const freePort = await findFreePort(port) if (port !== freePort) { - const opt = showDialog({ - title: i18n.t('multipleBusyPortsDialog.title'), - message: i18n.t('multipleBusyPortsDialog.message'), - type: 'error', - buttons: [ - i18n.t('multipleBusyPortsDialog.action'), - i18n.t('close') - ] - }) - - if (opt === 0) { - shell.openPath(join(ipfsd.path, 'config')) + const openConfig = dialogs.multipleBusyPortsDialog() + if (openConfig) { + shell.openPath(getConfigFilePath(ipfsd)) } - throw new Error('ports already being used') + return false } } + + return true } +/** + * Check if ports are available and handle it. Returns + * true if ports are cleared for IPFS to start. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {Promise} + */ async function checkPorts (ipfsd) { const config = readConfigFile(ipfsd) @@ -238,19 +329,19 @@ async function checkPorts (ipfsd) { return checkPortsArray(ipfsd, [].concat(config.Addresses.API, config.Addresses.Gateway)) } - const configApiMa = parseCfgMultiaddr(config.Addresses.API) - const configGatewayMa = parseCfgMultiaddr(config.Addresses.Gateway) + const configApiMa = parseMultiaddr(config.Addresses.API) + const configGatewayMa = parseMultiaddr(config.Addresses.Gateway) const isApiMaDaemon = await checkIfAddrIsDaemon(configApiMa.nodeAddress()) const isGatewayMaDaemon = await checkIfAddrIsDaemon(configGatewayMa.nodeAddress()) if (isApiMaDaemon && isGatewayMaDaemon) { logger.info('[daemon] ports busy by a daemon') - return + return true } - const apiPort = parseInt(configApiMa.nodeAddress().port, 10) - const gatewayPort = parseInt(configGatewayMa.nodeAddress().port, 10) + const apiPort = configApiMa.nodeAddress().port + const gatewayPort = configGatewayMa.nodeAddress().port const freeGatewayPort = await findFreePort(gatewayPort) let freeApiPort = await findFreePort(apiPort) @@ -264,53 +355,28 @@ async function checkPorts (ipfsd) { const busyGatewayPort = gatewayPort !== freeGatewayPort if (!busyApiPort && !busyGatewayPort) { - return + return true } // two "0" in config mean "pick free ports without any prompt" const promptUser = (apiPort !== 0 || gatewayPort !== 0) if (promptUser) { - let message = null - let options = null + let useAlternativePorts = null if (busyApiPort && busyGatewayPort) { logger.info('[daemon] api and gateway ports busy') - message = 'busyPortsDialog' - options = { - port1: apiPort, - alt1: freeApiPort, - port2: gatewayPort, - alt2: freeGatewayPort - } + useAlternativePorts = dialogs.busyPortsDialog(apiPort, freeApiPort, gatewayPort, freeGatewayPort) } else if (busyApiPort) { logger.info('[daemon] api port busy') - message = 'busyPortDialog' - options = { - port: apiPort, - alt: freeApiPort - } + useAlternativePorts = dialogs.busyPortDialog(apiPort, freeApiPort) } else { logger.info('[daemon] gateway port busy') - message = 'busyPortDialog' - options = { - port: gatewayPort, - alt: freeGatewayPort - } + useAlternativePorts = dialogs.busyPortDialog(gatewayPort, freeGatewayPort) } - const opt = showDialog({ - title: i18n.t(`${message}.title`), - message: i18n.t(`${message}.message`, options), - type: 'error', - buttons: [ - i18n.t(`${message}.action`, options), - i18n.t('close') - ] - }) - - if (opt !== 0) { - throw new Error('ports already being used') + if (!useAlternativePorts) { + return false } } @@ -324,9 +390,16 @@ async function checkPorts (ipfsd) { writeConfigFile(ipfsd, config) logger.info('[daemon] ports updated') + return true } -function checkValidConfig (ipfsd) { +/** + * Checks if the repository and the configuration file are valid. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {boolean} + */ +function checkRepositoryAndConfiguration (ipfsd) { if (!fs.pathExistsSync(ipfsd.path)) { // If the repository doesn't exist, skip verification. return true @@ -335,68 +408,47 @@ function checkValidConfig (ipfsd) { try { const stats = fs.statSync(ipfsd.path) if (!stats.isDirectory()) { - throw new Error('IPFS_PATH must be a directory') + logger.error(`${ipfsd.path} must be a directory`) + dialogs.repositoryMustBeDirectoryDialog(ipfsd.path) + return false } - if (!configExists(ipfsd)) { - // Config is generated automatically if it doesn't exist. - return true + if (!apiFileExists(ipfsd)) { + if (!configExists(ipfsd)) { + // Config is generated automatically if it doesn't exist. + logger.error(`configuration does not exist at ${ipfsd.path}`) + dialogs.repositoryConfigurationIsMissingDialog(ipfsd.path) + return true + } + + // This should catch errors such having no configuration file, + // IPFS_DIR not being a directory, or the configuration file + // being corrupted. + readConfigFile(ipfsd) + } + + const swarmKeyPath = join(ipfsd.path, 'swarm.key') + if (fs.pathExistsSync(swarmKeyPath)) { + // IPFS Desktop does not support private network IPFS repositories. + dialogs.repositoryIsPrivateDialog(ipfsd.path) + return false } - // This should catch errors such having no configuration file, - // IPFS_DIR not being a directory, or the configuration file - // being corrupted. - readConfigFile(ipfsd) return true } catch (e) { // Save to error.log logger.error(e) - - // Hide other windows so the user focus in on the dialog - BrowserWindow.getAllWindows().forEach(w => w.hide()) - - // Show blocking dialog - showDialog({ - title: i18n.t('invalidRepositoryDialog.title'), - message: i18n.t('invalidRepositoryDialog.message', { path: ipfsd.path }), - buttons: [i18n.t('quit')] - }) - - // Only option is to quit - app.quit() + dialogs.repositoryIsInvalidDialog(ipfsd.path) + return false } } -function checkPublicNetwork (ipfsd) { - const swarmKeyPath = join(ipfsd.path, 'swarm.key') - const swarmKeyExists = fs.pathExistsSync(swarmKeyPath) - - if (!swarmKeyExists) { - return true - } - - // Hide other windows so the user focus in on the dialog - BrowserWindow.getAllWindows().forEach(w => w.hide()) - - // Show blocking dialog - showDialog({ - title: i18n.t('privateNetworkDialog.title'), - message: i18n.t('privateNetworkDialog.message', { path: ipfsd.path }), - buttons: [i18n.t('quit')] - }) - - // Only option is to quit - app.quit() -} - module.exports = Object.freeze({ - configPath, configExists, apiFileExists, - rmApiFile, + removeApiFile, applyDefaults, migrateConfig, checkPorts, - checkValidConfig, - checkPublicNetwork + checkRepositoryAndConfiguration }) diff --git a/src/daemon/consts.js b/src/daemon/consts.js index 19619a9eb..8aa11097c 100644 --- a/src/daemon/consts.js +++ b/src/daemon/consts.js @@ -1,10 +1,16 @@ -module.exports = Object.freeze({ - STATUS: { - STARTING_STARTED: 1, - STARTING_FINISHED: 2, - STARTING_FAILED: 3, - STOPPING_STARTED: 4, - STOPPING_FINISHED: 5, - STOPPING_FAILED: 6 - } +/** + * IPFS daemon status codes for display in the UI. + * + * @type {Object.} + * @readonly + */ +const STATUS = Object.freeze({ + STARTING_STARTED: 1, + STARTING_FINISHED: 2, + STARTING_FAILED: 3, + STOPPING_STARTED: 4, + STOPPING_FINISHED: 5, + STOPPING_FAILED: 6 }) + +module.exports = { STATUS } diff --git a/src/daemon/daemon.js b/src/daemon/daemon.js index 614e64be3..88edbfb65 100644 --- a/src/daemon/daemon.js +++ b/src/daemon/daemon.js @@ -1,22 +1,16 @@ const Ctl = require('ipfsd-ctl') -const i18n = require('i18next') -const { showDialog } = require('../dialogs') const logger = require('../common/logger') const { getCustomBinary } = require('../custom-ipfs-binary') -const { applyDefaults, migrateConfig, checkPorts, configExists, checkValidConfig, checkPublicNetwork, rmApiFile, apiFileExists } = require('./config') +const { applyDefaults, migrateConfig, checkPorts, configExists, checkRepositoryAndConfiguration, removeApiFile, apiFileExists } = require('./config') const showMigrationPrompt = require('./migration-prompt') - -function cannotConnectDialog (addr) { - showDialog({ - title: i18n.t('cannotConnectToApiDialog.title'), - message: i18n.t('cannotConnectToApiDialog.message', { addr }), - type: 'error', - buttons: [ - i18n.t('close') - ] - }) -} - +const dialogs = require('./dialogs') +const { app } = require('electron') + +/** + * Get the IPFS binary file path. + * + * @returns {string} + */ function getIpfsBinPath () { return process.env.IPFS_GO_EXEC || getCustomBinary() || @@ -25,7 +19,16 @@ function getIpfsBinPath () { .replace('app.asar', 'app.asar.unpacked') } -async function spawn ({ flags, path }) { +/** + * Gets the IPFS daemon controller. If null is returned, + * it means that the repository or some configuration is wrong + * and IPFS Desktop should quit. + * + * @param {string[]} flags + * @param {string} path + * @returns {Promise} + */ +async function getIpfsd (flags, path) { const ipfsBin = getIpfsBinPath() const ipfsd = await Ctl.createController({ @@ -40,29 +43,37 @@ async function spawn ({ flags, path }) { args: flags }) - if (!checkValidConfig(ipfsd)) { - throw new Error(`repository at ${ipfsd.path} is invalid`) + // Checks if the repository is valid to use with IPFS Desktop. If not, + // we quit the app. We assume that checkRepositoryAndConfiguration + // presents any dialog explaining the situation. + if (!checkRepositoryAndConfiguration(ipfsd)) { + return null } - if (!checkPublicNetwork(ipfsd)) { - throw new Error(`repository at ${ipfsd.path} is part of private network`) - } + let isRemote = false if (configExists(ipfsd)) { migrateConfig(ipfsd) - return { ipfsd, isRemote: false } + } else { + // If config does not exist, but $IPFS_PATH/api exists + // then it is a remote repository. + isRemote = apiFileExists(ipfsd) + if (!isRemote) { + // It's a new repository! + await ipfsd.init() + applyDefaults(ipfsd) + } } - // If config does not exist, but $IPFS_PATH/api exists, then - // it is a remote repository. - if (apiFileExists(ipfsd)) { - return { ipfsd, isRemote: true } + if (!isRemote) { + // Check if ports are free and we're clear to start IPFS. + // If not, we return null. + if (!await checkPorts(ipfsd)) { + return null + } } - await ipfsd.init() - - applyDefaults(ipfsd) - return { ipfsd, isRemote: false } + return ipfsd } function listenToIpfsLogs (ipfsd, callback) { @@ -96,6 +107,19 @@ function listenToIpfsLogs (ipfsd, callback) { return stop } +/** + * @typedef {object} IpfsLogs + * @property {string} logs + * @property {string|undefined} id + * @property {any} err + */ + +/** + * Start IPFS, collects the logs, detects errors and migrations. + * + * @param {import('ipfsd-ctl').Controller} ipfsd + * @returns {Promise} + */ async function startIpfsWithLogs (ipfsd) { let err, id, migrationPrompt let isMigrating, isErrored, isFinished @@ -176,42 +200,41 @@ async function startIpfsWithLogs (ipfsd) { } } -module.exports = async function (opts) { - let ipfsd, isRemote - - try { - const res = await spawn(opts) - ipfsd = res.ipfsd - isRemote = res.isRemote - } catch (err) { - return { err } +/** + * Start the IPFS daemon. + * + * @param {any} opts + * @returns {Promise<{ ipfsd: import('ipfsd-ctl').Controller|undefined } & IpfsLogs>} + */ +async function startDaemon (opts) { + const ipfsd = await getIpfsd(opts.flags, opts.path) + if (ipfsd === null) { + app.quit() + return { ipfsd: undefined, err: new Error('get ipfsd failed'), id: undefined, logs: '' } } - if (!isRemote) { - try { - await checkPorts(ipfsd) - } catch (err) { - return { err } - } - } - - let errLogs = await startIpfsWithLogs(ipfsd) - - if (errLogs.err) { - if (!errLogs.err.message.includes('ECONNREFUSED') && !errLogs.err.message.includes('ERR_CONNECTION_REFUSED')) { - return { ipfsd, err: errLogs.err, logs: errLogs.logs } + let { err, logs, id } = await startIpfsWithLogs(ipfsd) + if (err) { + if (!err.message.includes('ECONNREFUSED') && !err.message.includes('ERR_CONNECTION_REFUSED')) { + return { ipfsd, err, logs, id } } if (!configExists(ipfsd)) { - cannotConnectDialog(ipfsd.apiAddr.toString()) - return { ipfsd, err: errLogs.err, logs: errLogs.logs } + dialogs.cannotConnectToApiDialog(ipfsd.apiAddr.toString()) + return { ipfsd, err, logs, id } } logger.info('[daemon] removing api file') - rmApiFile(ipfsd) + removeApiFile(ipfsd) - errLogs = await startIpfsWithLogs(ipfsd) + const errLogs = await startIpfsWithLogs(ipfsd) + err = errLogs.err + logs = errLogs.logs + id = errLogs.id } - return { ipfsd, err: errLogs.err, logs: errLogs.logs, id: errLogs.id } + // If we have an error here, it should have been handled by startIpfsWithLogs. + return { ipfsd, err, logs, id } } + +module.exports = startDaemon diff --git a/src/daemon/dialogs.js b/src/daemon/dialogs.js new file mode 100644 index 000000000..ef3212c42 --- /dev/null +++ b/src/daemon/dialogs.js @@ -0,0 +1,155 @@ +const { BrowserWindow } = require('electron') +const i18n = require('i18next') +const { showDialog } = require('../dialogs') + +function hideOtherWindows () { + // Hide other windows so the user focus in on the dialog + BrowserWindow.getAllWindows().forEach(w => w.hide()) +} + +/** + * Dialog to show when the daemon cannot connect to remote API. + * + * @param {string} addr + */ +function cannotConnectToApiDialog (addr) { + hideOtherWindows() + showDialog({ + title: i18n.t('cannotConnectToApiDialog.title'), + message: i18n.t('cannotConnectToApiDialog.message', { addr }), + type: 'error', + buttons: [ + i18n.t('quit') + ] + }) +} + +/** + * Dialog to show when there are multiple busy ports. + * + * @returns {boolean} open the configuration file + */ +function multipleBusyPortsDialog () { + hideOtherWindows() + const opt = showDialog({ + title: i18n.t('multipleBusyPortsDialog.title'), + message: i18n.t('multipleBusyPortsDialog.message'), + type: 'error', + buttons: [ + i18n.t('multipleBusyPortsDialog.action'), + i18n.t('quit') + ] + }) + + return opt === 0 +} + +/** + * Dialog to show when there is a busy port and we offer an alternative. + * + * @param {Number} port is the busy port + * @param {Number} alt is the alternative free port + * @returns {boolean} use the alternative port + */ +function busyPortDialog (port, alt) { + hideOtherWindows() + const opt = showDialog({ + title: i18n.t('busyPortDialog.title'), + message: i18n.t('busyPortDialog.message', { port, alt }), + type: 'error', + buttons: [ + i18n.t('busyPortDialog.action', { port, alt }), + i18n.t('quit') + ] + }) + + return opt === 0 +} + +/** + * Dialog to show when there are two busy ports and we offer an alternative. + * + * @param {Number} port1 is the busy port 1 + * @param {Number} alt1 is the alternative free port 1 + * @param {Number} port2 is the busy port 2 + * @param {Number} alt2 is the alternative free port 2 + * @returns {boolean} use the alternative port + */ +function busyPortsDialog (port1, alt1, port2, alt2) { + hideOtherWindows() + const opt = showDialog({ + title: i18n.t('busyPortsDialog.title'), + message: i18n.t('busyPortsDialog.message', { port1, alt1, port2, alt2 }), + type: 'error', + buttons: [ + i18n.t('busyPortsDialog.action', { port1, alt1, port2, alt2 }), + i18n.t('quit') + ] + }) + + return opt === 0 +} + +/** + * Show the dialog with the text from the i18nKey, using the + * options opts. + * + * @param {string} i18nKey + * @param {any} opts + */ +function hideWindowsAndShowDialog (i18nKey, opts) { + hideOtherWindows() + showDialog({ + title: i18n.t(`${i18nKey}.title`), + message: i18n.t(`${i18nKey}.message`, opts), + buttons: [i18n.t('quit')] + }) +} + +/** + * Dialog to show when the repository is part of a private network. + * + * @param {string} path + */ +function repositoryIsPrivateDialog (path) { + hideWindowsAndShowDialog('privateNetworkDialog', { path }) +} + +/** + * Dialog to show when we detect that the repository is not a directory. + * + * @param {string} path + */ +function repositoryMustBeDirectoryDialog (path) { + hideWindowsAndShowDialog('repositoryMustBeDirectoryDialog', { path }) +} + +/** + * Dialog to show when we detect that the configuration file is missing. + * + * @param {string} path + */ +function repositoryConfigurationIsMissingDialog (path) { + hideWindowsAndShowDialog('repositoryConfigurationIsMissingDialog', { path }) +} + +/** + * Dialog to show when we detect that the repository is invalid, but we + * are not sure what the problem is. + * + * @param {string} path + */ +function repositoryIsInvalidDialog (path) { + hideWindowsAndShowDialog('invalidRepositoryDialog', { path }) +} + +module.exports = { + cannotConnectToApiDialog, + multipleBusyPortsDialog, + busyPortDialog, + busyPortsDialog, + repositoryIsPrivateDialog, + repositoryMustBeDirectoryDialog, + repositoryConfigurationIsMissingDialog, + repositoryIsInvalidDialog +} diff --git a/src/daemon/index.js b/src/daemon/index.js index 887a7deac..7a8bef764 100644 --- a/src/daemon/index.js +++ b/src/daemon/index.js @@ -9,7 +9,7 @@ const createDaemon = require('./daemon') const ipcMainEvents = require('../common/ipc-main-events') const { analyticsKeys } = require('../analytics/keys') -module.exports = async function (ctx) { +async function setupDaemon (ctx) { let ipfsd = null let status = null let wasOnline = null @@ -124,4 +124,5 @@ module.exports = async function (ctx) { }) } +module.exports = setupDaemon module.exports.STATUS = STATUS diff --git a/src/daemon/migration-prompt.js b/src/daemon/migration-prompt.js index 9784af9ba..ecb4b2560 100644 --- a/src/daemon/migration-prompt.js +++ b/src/daemon/migration-prompt.js @@ -83,7 +83,21 @@ const errorTemplate = (logs) => { let window -module.exports = (logs, error = false, done = false) => { +/** + * @typedef {object} MigrationPrompt + * @property {(logs: string) => boolean} update + * @property {(logs: string, error: boolean, done: boolean) => void} loadWindow + */ + +/** + * Show migration prompt. + * + * @param {string} logs + * @param {boolean} error + * @param {boolean} done + * @returns {MigrationPrompt} + */ +function showMigrationPrompt (logs, error = false, done = false) { // Generate random id const id = crypto.randomBytes(16).toString('hex') @@ -130,3 +144,5 @@ module.exports = (logs, error = false, done = false) => { loadWindow } } + +module.exports = showMigrationPrompt