diff --git a/public/electron.js b/public/electron.js index 3595d01..51dd80c 100644 --- a/public/electron.js +++ b/public/electron.js @@ -13,6 +13,12 @@ const spawn = require('child_process').spawn; const fs = require('fs/promises'); const ip = require('ip'); +let nodeInstance = null; +let intervalId = -1; +let nodeRunning = false; + +class NodeAlreadyRunningError extends Error {} + const networks = { mainnet: { protocolMagic: 764824073, @@ -96,6 +102,10 @@ const unpackArchive = (directory) => const startNode = (directory, network) => new Promise((resolve, reject) => { + if (nodeInstance !== null) { + throw new NodeAlreadyRunningError(); + } + const nodeBinary = nodeBinaries[process.platform]; const configDirectory = path.join(directory, `${network}-config`); const databaseDirectory = path.join(directory, `${network}-db`); @@ -104,7 +114,7 @@ const startNode = (directory, network) => nodeBinary.replace('.tar.gz', '').replace('.zip', '') ); - const child = spawn(path.join(binaryPath, 'cardano-node'), [ + nodeInstance = spawn(path.join(binaryPath, 'cardano-node'), [ 'run', '--topology', path.join(configDirectory, 'topology.json'), @@ -119,20 +129,20 @@ const startNode = (directory, network) => '--config', path.join(configDirectory, 'config.json'), ]); - child.on('close', (code) => { - console.log(`child process exited with code ${code}`); - if (code === 0) { - resolve(); - } else { - reject(); - } + + nodeInstance.on('error', (error) => { + console.log(`child process exited with error ${error.name}`); + console.log(error.message); + reject(); }); - child.stdout.on('data', (data) => { + nodeInstance.on('spawn', () => resolve()); + + nodeInstance.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); - child.stderr.on('data', (data) => { + nodeInstance.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); }); @@ -197,8 +207,6 @@ function createWindow() { return mainWindow; } -let intervalId = -1; - app.whenReady().then(() => { const mainWindow = createWindow(); @@ -231,7 +239,21 @@ app.whenReady().then(() => { } }); + ipcMain.on('stop-node', () => { + if (nodeRunning) { + nodeRunning = false; + mainWindow.webContents.send('node-status', { + id: -1, + timestamp: new Date().getTime(), + status: 'shutdown', + message: 'stopping the node ⏸ #{...}', + }); + } + }); + ipcMain.on('start-node', async (_event, directory, network = 'mainnet') => { + nodeRunning = true; + mainWindow.webContents.send('node-status', { id: 0, timestamp: new Date().getTime(), @@ -261,6 +283,17 @@ app.whenReady().then(() => { return; } + if (!nodeRunning) { + mainWindow.webContents.send('node-status', { + id: -1, + timestamp: new Date().getTime(), + status: 'idle', + message: 'the node has been successfully stopped ✅!', + }); + + return; + } + mainWindow.webContents.send('node-status', { id: 0, timestamp: new Date().getTime(), @@ -275,6 +308,15 @@ app.whenReady().then(() => { message: 'download configuration and node topology #{...}', }); await downloadConfig(directory, network); + if (!nodeRunning) { + mainWindow.webContents.send('node-status', { + id: -1, + timestamp: new Date().getTime(), + status: 'idle', + message: 'the node has been successfully stopped ✅!', + }); + return; + } mainWindow.webContents.send('node-status', { id: 1, timestamp: new Date().getTime(), @@ -310,7 +352,48 @@ app.whenReady().then(() => { message: 'node initialization running #{...}', }); - startNode(directory, network); + try { + await startNode(directory, network); + } catch (error) { + if (error instanceof NodeAlreadyRunningError) { + mainWindow.webContents.send('node-status', { + id: 2, + timestamp: new Date().getTime(), + status: 'running', + message: 'node already running', + }); + } else { + mainWindow.webContents.send('node-status', { + id: 2, + timestamp: new Date().getTime(), + status: 'error', + message: `failed to start node${ + typeof error.message !== 'undefined' && ': ' + error.message + }`, + }); + } + return; + } + + if (nodeRunning) { + mainWindow.webContents.send('node-status', { + id: 2, + timestamp: new Date().getTime(), + status: 'running', + message: 'the node is up and running 🚀!', + }); + } else { + nodeInstance.kill('SIGINT'); + nodeInstance = null; + mainWindow.webContents.send('node-status', { + id: -1, + timestamp: new Date().getTime(), + status: 'idle', + message: 'the node has been successfully stopped ✅!', + }); + return; + } + const nodeBinary = nodeBinaries[process.platform]; const binaryPath = path.join( directory, @@ -318,6 +401,19 @@ app.whenReady().then(() => { ); const getCurrentTip = () => { + if (!nodeRunning) { + mainWindow.webContents.send('node-status', { + id: -1, + timestamp: new Date().getTime(), + status: 'idle', + message: 'the node has been successfully stopped ✅!', + }); + nodeInstance.kill('SIGINT'); + nodeInstance = null; + clearInterval(intervalId); + intervalId = -1; + return; + } const databaseDirectory = path.join(directory, `${network}-db`); const args = ['query', 'tip']; @@ -349,6 +445,7 @@ app.whenReady().then(() => { } } }); + child.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); diff --git a/public/preload.js b/public/preload.js index 6d04916..0507d31 100644 --- a/public/preload.js +++ b/public/preload.js @@ -22,4 +22,5 @@ contextBridge.exposeInMainWorld('electron', { getDefaultDirectory: () => ipcRenderer.invoke('getDefaultPath'), startNode: (directory, network) => ipcRenderer.send('start-node', directory, network), + stopNode: () => ipcRenderer.send('stop-node'), }); diff --git a/src/global/types.ts b/src/global/types.ts index df62295..48b005b 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1,6 +1,6 @@ export type MessageType = 'success' | 'error' | 'warning' | 'info'; -export type NodeStatus = 'idle' | 'download' | 'running' | 'error'; +export type NodeStatus = 'idle' | 'download' | 'running' | 'error' | 'shutdown'; export type IpcEventListener = ( channel: string, callback: (argument: any) => void diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c399a7b..1c31861 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -105,8 +105,19 @@ const Dashboard = () => { const startNode = (window as any).electron?.startNode; if (typeof startNode === 'function') { - startNode(directory, selectedNetwork); setNodeRunning(true); + setNodeLog([ + { status: 'idle', message: 'cardano-node-ui:~$', id: -1, timestamp: 0 }, + ]); + startNode(directory, selectedNetwork); + } + }; + + const stopNode = () => { + const stopNode = (window as any).electron?.stopNode; + if (typeof stopNode === 'function') { + stopNode(); + setNodeRunning(false); } }; @@ -125,7 +136,7 @@ const Dashboard = () => { ); formattedMessage = formattedMessage.replaceAll('#{:x:}', '❌'); - if (ids.includes(line.id)) { + if (ids.includes(line.id) && line.id !== -1) { const timestamp = Object.keys(messages).find( (key) => messages[key as unknown as number].id === line.id ) as unknown as number; @@ -198,6 +209,45 @@ const Dashboard = () => { setSnackbarOpen(true); }; + const getActionButton = () => { + if (nodeStatus === 'idle' || nodeStatus === 'error') { + return ( + + ); + } else if (nodeStatus === 'shutdown') { + return ( + + ); + } else { + return ( + + ); + } + }; + return ( { Preview - + {getActionButton()}