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()}