diff --git a/.nonsense b/.nonsense deleted file mode 100644 index d897fd8a1e..0000000000 --- a/.nonsense +++ /dev/null @@ -1,3 +0,0 @@ -------- Ignore this file, it's created/changed to trigger build ------- -• -yeah diff --git a/CHANGELOG.md b/CHANGELOG.md index 899c220979..669cb9ace6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## vNext ### Features + - Implemented "Catalyst Fund7" voting registration changes ([PR 2732](https://github.com/input-output-hk/daedalus/pull/2732)) - Added Over-saturation warning in delegation wizard ([PR 2733](https://github.com/input-output-hk/daedalus/pull/2733)) - Added Catalyst footer links ([PR 2721](https://github.com/input-output-hk/daedalus/pull/2721)) @@ -18,6 +19,7 @@ ### Chores +- Improves the Daedalus startup by avoiding unnecessary cardano-node restarts ([PR 2716](https://github.com/input-output-hk/daedalus/pull/2716)) - Updated `cardano-launcher` to version `0.20211105.1` and added Cardano Node RTS flags which improve resource usage ([PR 2734](https://github.com/input-output-hk/daedalus/pull/2734)) - Updated README with solution steps for the nix SSL issue ([PR 2727](https://github.com/input-output-hk/daedalus/pull/2727)) - Covered LedgerJS v4.0.0 breaking changes ([PR 2697](https://github.com/input-output-hk/daedalus/pull/2697)) diff --git a/source/common/types/no-disk-space.types.js b/source/common/types/no-disk-space.types.js index 9453ab7fe4..361f289c1d 100644 --- a/source/common/types/no-disk-space.types.js +++ b/source/common/types/no-disk-space.types.js @@ -5,4 +5,5 @@ export type CheckDiskSpaceResponse = { diskSpaceMissing: string, diskSpaceRecommended: string, diskSpaceAvailable: string, + hadNotEnoughSpaceLeft: boolean, }; diff --git a/source/main/cardano/CardanoNode.js b/source/main/cardano/CardanoNode.js index b7e2dd6859..e8244282e7 100644 --- a/source/main/cardano/CardanoNode.js +++ b/source/main/cardano/CardanoNode.js @@ -240,11 +240,18 @@ export class CardanoNode { * @param log * @param actions * @param transitions + * @param config {CardanoNodeConfig} */ - constructor(log: Logger, actions: Actions, transitions: StateTransitions) { + constructor( + log: Logger, + actions: Actions, + transitions: StateTransitions, + config: CardanoNodeConfig + ) { this._log = log; this._actions = actions; this._transitionListeners = transitions; + this._config = config; } /** @@ -254,14 +261,10 @@ export class CardanoNode { * Asks the node to reply with the current port. * Transitions into STARTING state. * - * @param config {CardanoNodeConfig} * @param isForced {boolean} * @returns {Promise} resolves if the node could be started, rejects with error otherwise. */ - start = async ( - config: CardanoNodeConfig, - isForced: boolean = false - ): Promise => { + start = async (isForced: boolean = false): Promise => { const { _log } = this; // Guards @@ -273,29 +276,13 @@ export class CardanoNode { }); return Promise.reject(new Error('CardanoNode: Cannot be started')); } - if (this._isUnrecoverable(config) && !isForced) { + if (this._isUnrecoverable(this._config) && !isForced) { _log.error('CardanoNode#start: Too many startup retries', { startupTries: this._startupTries, }); return Promise.reject(new Error('CardanoNode: Too many startup retries')); } - // Setup - const { - // startupTimeout, - nodeConfig, - stateDir, - cluster, - tlsPath, - configPath, - syncTolerance, - cliBin, - isStaging, - metadataUrl, - } = config; - - this._config = config; - this._startupTries++; this._changeToState(CardanoNodeStates.STARTING); _log.info( @@ -313,7 +300,7 @@ export class CardanoNode { }, { size: '5M', - path: config.logFilePath, + path: this._config.logFilePath, maxFiles: 4, } ); @@ -328,7 +315,7 @@ export class CardanoNode { }, { size: '5M', - path: config.logFilePath, + path: this._config.logFilePath, maxFiles: 4, } ); @@ -356,7 +343,7 @@ export class CardanoNode { { error } ); const { code, signal } = error || {}; - this._handleCardanoNodeError(code, signal); + await this._handleCardanoNodeError(code, signal); reject( new Error( 'CardanoNode#start: Unable to initialize cardano-launcher' @@ -366,18 +353,10 @@ export class CardanoNode { } else { try { const node = await CardanoWalletLauncher({ + ...this._config, nodeImplementation, - nodeConfig, - cluster, - stateDir, - tlsPath, - configPath, - syncTolerance, nodeLogFile, walletLogFile, - cliBin, - isStaging, - metadataUrl, }); this._node = node; @@ -424,7 +403,7 @@ export class CardanoNode { }); resolve(); }) - .catch((exitStatus) => { + .catch(async (exitStatus) => { _log.error( 'CardanoNode#start: Error while spawning cardano-node', { @@ -432,7 +411,7 @@ export class CardanoNode { } ); const { code, signal } = exitStatus.wallet || {}; - this._handleCardanoNodeError(code, signal); + await this._handleCardanoNodeError(code, signal); reject( new Error( 'CardanoNode#start: Error while spawning cardano-node' @@ -447,7 +426,7 @@ export class CardanoNode { } ); const { code, signal } = error || {}; - this._handleCardanoNodeError(code, signal); + await this._handleCardanoNodeError(code, signal); reject( new Error( 'CardanoNode#start: Unable to initialize cardano-launcher' @@ -477,6 +456,7 @@ export class CardanoNode { if (_node) await _node.stop(_config.shutdownTimeout / 1000); await this._waitForNodeProcessToExit(_config.shutdownTimeout); await this._storeProcessStates(); + this._changeToState(CardanoNodeStates.STOPPED); this._reset(); return Promise.resolve(); } catch (error) { @@ -529,7 +509,7 @@ export class CardanoNode { * @returns {Promise} resolves if the node could be restarted, rejects with error otherwise. */ async restart(isForced: boolean = false): Promise { - const { _log, _config } = this; + const { _log } = this; try { // Stop cardano nicely if it is still awake if (this._isConnected()) { @@ -546,14 +526,13 @@ export class CardanoNode { }); safeExitWithCode(0); } else { - await this.start(_config, isForced); + await this.start(isForced); } } catch (error) { - _log.error('CardanoNode#restart: Could not restart cardano-node', { - error, - }); + _log.error('CardanoNode#restart: Could not restart cardano-node', error); if (this._state !== CardanoNodeStates.UNRECOVERABLE) { - this._changeToState(CardanoNodeStates.ERRORED); + const { code, signal } = error || {}; + this._changeToState(CardanoNodeStates.ERRORED, code, signal); } return Promise.reject(error); } @@ -706,7 +685,12 @@ export class CardanoNode { } else { this._changeToState(CardanoNodeStates.ERRORED); this._transitionListeners.onError(code, signal); - await this.restart(); + try { + _log.info('CardanoNode: restarting'); + await this.restart(); + } catch (error) { + _log.error('CardanoNode: cannot be restarted', error); + } } }; @@ -780,6 +764,8 @@ export class CardanoNode { return _transitionListeners.onUpdated(); case CardanoNodeStates.CRASHED: return _transitionListeners.onCrashed(...args); + case CardanoNodeStates.ERRORED: + return _transitionListeners.onError(...args); case CardanoNodeStates.UNRECOVERABLE: return _transitionListeners.onUnrecoverable(); default: diff --git a/source/main/cardano/setup.js b/source/main/cardano/setup.js index 7a45cd299d..6028e680e5 100644 --- a/source/main/cardano/setup.js +++ b/source/main/cardano/setup.js @@ -34,10 +34,25 @@ import { } from '../ipc/cardano.ipc'; import { safeExitWithCode } from '../utils/safeExitWithCode'; -const startCardanoNode = ( - node: CardanoNode, - launcherConfig: LauncherConfig -) => { +const restartCardanoNode = async (node: CardanoNode) => { + try { + await node.restart(); + } catch (error) { + logger.error('Could not restart CardanoNode', { error }); + } +}; + +/** + * Configures, starts and manages the CardanoNode responding to node + * state changes, app events and IPC messages coming from the renderer. + * + * @param launcherConfig {LauncherConfig} + * @param mainWindow + */ +export const setupCardanoNode = ( + launcherConfig: LauncherConfig, + mainWindow: BrowserWindow +): CardanoNode => { const { logsPrefix, nodeImplementation, @@ -70,28 +85,7 @@ const startCardanoNode = ( killTimeout: NODE_KILL_TIMEOUT, updateTimeout: NODE_UPDATE_TIMEOUT, }; - return node.start(config); -}; - -const restartCardanoNode = async (node: CardanoNode) => { - try { - await node.restart(); - } catch (error) { - logger.error('Could not restart CardanoNode', { error }); - } -}; -/** - * Configures, starts and manages the CardanoNode responding to node - * state changes, app events and IPC messages coming from the renderer. - * - * @param launcherConfig {LauncherConfig} - * @param mainWindow - */ -export const setupCardanoNode = ( - launcherConfig: LauncherConfig, - mainWindow: BrowserWindow -): CardanoNode => { const cardanoNode = new CardanoNode( logger, { @@ -100,9 +94,9 @@ export const setupCardanoNode = ( exec, readFileSync, createWriteStream, - broadcastTlsConfig: (config: ?TlsConfig) => { + broadcastTlsConfig: (tlsConfig: ?TlsConfig) => { if (!mainWindow.isDestroyed()) - cardanoTlsConfigChannel.send(config, mainWindow); + cardanoTlsConfigChannel.send(tlsConfig, mainWindow); }, broadcastStateChange: (state: CardanoNodeState) => { if (!mainWindow.isDestroyed()) @@ -127,11 +121,10 @@ export const setupCardanoNode = ( }, onError: () => {}, onUnrecoverable: () => {}, - } + }, + config ); - startCardanoNode(cardanoNode, launcherConfig); - getCachedCardanoStatusChannel.onRequest(() => { logger.info('ipcMain: Received request from renderer for cardano status', { status: cardanoNode.status, diff --git a/source/main/config.js b/source/main/config.js index a0fe1d5040..45fbb6b581 100644 --- a/source/main/config.js +++ b/source/main/config.js @@ -154,9 +154,9 @@ export const MAX_LAUNCHER_LOGS_ALLOWED = 3; // CardanoNode config export const NODE_STARTUP_TIMEOUT = 5000; export const NODE_STARTUP_MAX_RETRIES = 5; -export const NODE_SHUTDOWN_TIMEOUT = isTest ? 5000 : 10000; -export const NODE_KILL_TIMEOUT = isTest ? 5000 : 10000; -export const NODE_UPDATE_TIMEOUT = isTest ? 10000 : 60000; +export const NODE_SHUTDOWN_TIMEOUT = isTest ? 5000 : 5 * 60 * 1000; // 5 minutes | unit: milliseconds +export const NODE_KILL_TIMEOUT = isTest ? 5000 : 5 * 60 * 1000; // 5 minutes | unit: milliseconds +export const NODE_UPDATE_TIMEOUT = isTest ? 10000 : 5 * 60 * 1000; // 5 minutes | unit: milliseconds export const DISK_SPACE_REQUIRED = 2 * 1073741274; // 2 GB | unit: bytes export const DISK_SPACE_REQUIRED_MARGIN_PERCENTAGE = 10; // 10% of the available disk space diff --git a/source/main/index.js b/source/main/index.js index 1bde4619fd..fe9282d040 100644 --- a/source/main/index.js +++ b/source/main/index.js @@ -35,7 +35,6 @@ import { getStateDirectoryPathChannel } from './ipc/getStateDirectoryPathChannel import { getDesktopDirectoryPathChannel } from './ipc/getDesktopDirectoryPathChannel'; import { getSystemLocaleChannel } from './ipc/getSystemLocaleChannel'; import { CardanoNodeStates } from '../common/types/cardano-node.types'; -import type { CheckDiskSpaceResponse } from '../common/types/no-disk-space.types'; import type { GenerateWalletMigrationReportRendererRequest, SetStateSnapshotLogMainResponse, @@ -54,7 +53,7 @@ import { // Global references to windows to prevent them from being garbage collected let mainWindow: BrowserWindow; -let cardanoNode: ?CardanoNode; +let cardanoNode: CardanoNode; const { isDev, @@ -154,52 +153,39 @@ const onAppReady = async () => { await installChromeExtensions(isDev); // Detect locale - let locale = getLocale(network); + const locale = getLocale(network); mainWindow = createMainWindow( locale, restoreSavedWindowBounds(screen, requestElectronStore) ); saveWindowBoundsOnSizeAndPositionChange(mainWindow, requestElectronStore); - const onCheckDiskSpace = ({ - isNotEnoughDiskSpace, - }: CheckDiskSpaceResponse) => { - if (cardanoNode) { - if (isNotEnoughDiskSpace) { - if ( - cardanoNode.state !== CardanoNodeStates.STOPPING && - cardanoNode.state !== CardanoNodeStates.STOPPED - ) { - try { - cardanoNode.stop(); - } catch (e) {} // eslint-disable-line - } - } else if ( - cardanoNode.state !== CardanoNodeStates.STARTING && - cardanoNode.state !== CardanoNodeStates.RUNNING - ) { - cardanoNode.restart(); - } - } - }; - const handleCheckDiskSpace = handleDiskSpace(mainWindow, onCheckDiskSpace); - const onMainError = (error: string) => { - if (error.indexOf('ENOSPC') > -1) { - handleCheckDiskSpace(); - return false; - } - }; - mainErrorHandler(onMainError); - await handleCheckDiskSpace(); + cardanoNode = setupCardanoNode(launcherConfig, mainWindow); - await handleCheckBlockReplayProgress(mainWindow, launcherConfig.logsPrefix); + buildAppMenus(mainWindow, cardanoNode, locale, { + isNavigationEnabled: false, + }); - cardanoNode = setupCardanoNode(launcherConfig, mainWindow); + enableApplicationMenuNavigationChannel.onReceive( + () => + new Promise((resolve) => { + buildAppMenus(mainWindow, cardanoNode, locale, { + isNavigationEnabled: true, + }); + resolve(); + }) + ); - if (isWatchMode) { - // Connect to electron-connect server which restarts / reloads windows on file changes - client.create(mainWindow); - } + rebuildApplicationMenu.onReceive( + (data) => + new Promise((resolve) => { + buildAppMenus(mainWindow, cardanoNode, locale, { + isNavigationEnabled: data.isNavigationEnabled, + }); + mainWindow.updateTitle(locale); + resolve(); + }) + ); setStateSnapshotLogChannel.onReceive( (data: SetStateSnapshotLogMainResponse) => { @@ -223,6 +209,22 @@ const onAppReady = async () => { getSystemLocaleChannel.onRequest(() => Promise.resolve(systemLocale)); + const handleCheckDiskSpace = handleDiskSpace(mainWindow, cardanoNode); + const onMainError = (error: string) => { + if (error.indexOf('ENOSPC') > -1) { + handleCheckDiskSpace(); + return false; + } + }; + mainErrorHandler(onMainError); + await handleCheckDiskSpace(); + await handleCheckBlockReplayProgress(mainWindow, launcherConfig.logsPrefix); + + if (isWatchMode) { + // Connect to electron-connect server which restarts / reloads windows on file changes + client.create(mainWindow); + } + mainWindow.on('close', async (event) => { logger.info( 'mainWindow received event. Safe exiting Daedalus now.' @@ -231,32 +233,6 @@ const onAppReady = async () => { await safeExit(); }); - buildAppMenus(mainWindow, cardanoNode, locale, { - isNavigationEnabled: false, - }); - - await enableApplicationMenuNavigationChannel.onReceive( - () => - new Promise((resolve) => { - buildAppMenus(mainWindow, cardanoNode, locale, { - isNavigationEnabled: true, - }); - resolve(); - }) - ); - - await rebuildApplicationMenu.onReceive( - (data) => - new Promise((resolve) => { - locale = getLocale(network); - buildAppMenus(mainWindow, cardanoNode, locale, { - isNavigationEnabled: data.isNavigationEnabled, - }); - mainWindow.updateTitle(locale); - resolve(); - }) - ); - // Security feature: Prevent creation of new browser windows // https://github.com/electron/electron/blob/master/docs/tutorial/security.md#14-disable-or-limit-creation-of-new-windows app.on('web-contents-created', (_, contents) => { diff --git a/source/main/utils/handleDiskSpace.js b/source/main/utils/handleDiskSpace.js index 148b4ce2c7..b8ef1d3c0f 100644 --- a/source/main/utils/handleDiskSpace.js +++ b/source/main/utils/handleDiskSpace.js @@ -13,16 +13,22 @@ import { DISK_SPACE_RECOMMENDED_PERCENTAGE, stateDirectoryPath, } from '../config'; +import { CardanoNodeStates } from '../../common/types/cardano-node.types'; +import { CardanoNode } from '../cardano/CardanoNode'; +import type { CheckDiskSpaceResponse } from '../../common/types/no-disk-space.types'; export const handleDiskSpace = ( mainWindow: BrowserWindow, - onCheckDiskSpace?: Function -) => { + cardanoNode: CardanoNode +): Function => { let diskSpaceCheckInterval; let diskSpaceCheckIntervalLength = DISK_SPACE_CHECK_LONG_INTERVAL; // Default check interval let isNotEnoughDiskSpace = false; // Default check state - const handleCheckDiskSpace = async (forceDiskSpaceRequired?: number) => { + const handleCheckDiskSpace = async ( + hadNotEnoughSpaceLeft: boolean, + forceDiskSpaceRequired?: number + ): Promise => { const diskSpaceRequired = forceDiskSpaceRequired || DISK_SPACE_REQUIRED; try { const { @@ -71,11 +77,55 @@ export const handleDiskSpace = ( diskSpaceMissing: prettysize(diskSpaceMissing), diskSpaceRecommended: prettysize(diskSpaceRecommended), diskSpaceAvailable: prettysize(diskSpaceAvailable), + hadNotEnoughSpaceLeft, }; - if (isNotEnoughDiskSpace) - logger.info('Not enough disk space', { response }); - if (typeof onCheckDiskSpace === 'function') onCheckDiskSpace(response); - getDiskSpaceStatusChannel.send(response, mainWindow.webContents); + + const NO_SPACE_AND_CARDANO_NODE_CAN_BE_STOPPED = + isNotEnoughDiskSpace && + cardanoNode.state !== CardanoNodeStates.STOPPING && + cardanoNode.state !== CardanoNodeStates.STOPPED; + + const CARDANO_NODE_CAN_BE_STARTED_FOR_THE_FIRST_TIME = + !isNotEnoughDiskSpace && + cardanoNode.state === CardanoNodeStates.STOPPED && + cardanoNode._startupTries === 0; + + const CARDANO_NODE_CAN_BE_STARTED_AFTER_FREEING_SPACE = + !isNotEnoughDiskSpace && + cardanoNode.state !== CardanoNodeStates.STOPPED && + cardanoNode.state !== CardanoNodeStates.STOPPING && + hadNotEnoughSpaceLeft; + + switch (true) { + case NO_SPACE_AND_CARDANO_NODE_CAN_BE_STOPPED: + try { + logger.info('[DISK-SPACE-DEBUG] Stopping cardano node'); + await cardanoNode.stop(); + } catch (error) { + logger.error('[DISK-SPACE-DEBUG] Cannot stop cardano node', error); + } + break; + case CARDANO_NODE_CAN_BE_STARTED_FOR_THE_FIRST_TIME: + await cardanoNode.start(); + break; + case CARDANO_NODE_CAN_BE_STARTED_AFTER_FREEING_SPACE: + try { + logger.info( + '[DISK-SPACE-DEBUG] restart cardano node after freeing up disk space' + ); + if (cardanoNode._startupTries > 0) await cardanoNode.restart(); + else await cardanoNode.start(); + response.hadNotEnoughSpaceLeft = false; + } catch (error) { + logger.error( + '[DISK-SPACE-DEBUG] Daedalus tried to restart, but failed', + error + ); + } + break; + default: + } + await getDiskSpaceStatusChannel.send(response, mainWindow.webContents); return response; } catch (error) { // Remove diskSpaceCheckInterval if set @@ -90,16 +140,20 @@ export const handleDiskSpace = ( diskSpaceMissing: '', diskSpaceRecommended: '', diskSpaceAvailable: '', + hadNotEnoughSpaceLeft: false, }; - getDiskSpaceStatusChannel.send(response, mainWindow.webContents); + await getDiskSpaceStatusChannel.send(response, mainWindow.webContents); return response; } }; + let hadNotEnoughSpaceLeft: boolean = false; + const setDiskSpaceIntervalChecking = (interval) => { clearInterval(diskSpaceCheckInterval); diskSpaceCheckInterval = setInterval(async () => { - handleCheckDiskSpace(); + const response = await handleCheckDiskSpace(hadNotEnoughSpaceLeft); + hadNotEnoughSpaceLeft = response.hadNotEnoughSpaceLeft; }, interval); diskSpaceCheckIntervalLength = interval; }; @@ -108,7 +162,7 @@ export const handleDiskSpace = ( setDiskSpaceIntervalChecking(diskSpaceCheckIntervalLength); getDiskSpaceStatusChannel.onReceive((diskSpaceRequired) => - handleCheckDiskSpace(diskSpaceRequired) + handleCheckDiskSpace(hadNotEnoughSpaceLeft, diskSpaceRequired) ); return handleCheckDiskSpace; diff --git a/source/main/utils/logging.js b/source/main/utils/logging.js index 99a33b88f9..d520853354 100644 --- a/source/main/utils/logging.js +++ b/source/main/utils/logging.js @@ -6,6 +6,7 @@ import type { FormatMessageContextParams, Logger, } from '../../common/types/logging.types'; +import { toJS } from '../../common/utils/helper'; const appName = 'daedalus'; const electronProcess = 'ipcMain'; @@ -28,7 +29,7 @@ const environmentData = { const logToLevel = (level: string) => (message: string, data: ?Object) => log[level](formatContext({ ...messageContext, level }), { message, - data, + data: toJS(data), environmentData, });