diff --git a/app/main.ts b/app/main.ts index 28f021e..8bb6f35 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions } from 'electron'; +import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions, IpcMainInvokeEvent } from 'electron'; import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -6,6 +6,44 @@ import * as https from 'https'; import { createHash } from 'crypto'; import * as tar from 'tar'; import * as os from 'os'; +import * as pidusage from 'pidusage'; + +interface Stats { + /** + * percentage (from 0 to 100*vcore) + */ + cpu: number; + + /** + * bytes + */ + memory: number; + + /** + * PPID + */ + ppid: number; + + /** + * PID + */ + pid: number; + + /** + * ms user + system time + */ + ctime: number; + + /** + * ms since the start of the process + */ + elapsed: number; + + /** + * ms since epoch + */ + timestamp: number; +} //import bz2 from 'unbzip2-stream'; //import * as bz2 from 'unbzip2-stream'; @@ -14,6 +52,7 @@ const bz2 = require('unbzip2-stream'); let win: BrowserWindow | null = null; let isHidden: boolean = false; let isQuitting: boolean = false; +let monerodProcess: ChildProcessWithoutNullStreams | null = null; const args = process.argv.slice(1), serve = args.some(val => val === '--serve'); @@ -133,28 +172,6 @@ function createWindow(): BrowserWindow { return win; } -function isWifiConnectedOld() { - console.log("isWifiConnected()"); - const networkInterfaces = os.networkInterfaces(); - - console.log(`isWifiConnected(): network interfaces ${networkInterfaces}`); - - for (const interfaceName in networkInterfaces) { - const networkInterface = networkInterfaces[interfaceName]; - - if (networkInterface) { - for (const network of networkInterface) { - if (network.family === 'IPv4' && !network.internal && network.mac !== '00:00:00:00:00:00') { - if (interfaceName.toLowerCase().includes('wifi') || interfaceName.toLowerCase().includes('wlan')) { - return true; - } - } - } - } - } - return false; -} - function isConnectedToWiFi(): Promise { return new Promise((resolve, reject) => { const platform = os.platform(); // Use os to get the platform @@ -229,8 +246,13 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr const monerodPath = commandOptions.shift(); if (!monerodPath) { - win?.webContents.send('monero-sterr', `Invalid monerod path provided: ${monerodPath}`); - throw Error("Invalid monerod path provided"); + win?.webContents.send('monero-stderr', `Invalid monerod path provided: ${monerodPath}`); + throw new Error("Invalid monerod path provided"); + } + + if (monerodProcess != null) { + win?.webContents.send('monero-stderr', 'Monerod already started'); + throw new Error("Monerod already started"); } console.log("Starting monerod daemon with options: " + commandOptions.join(" ")); @@ -238,7 +260,7 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr moneroFirstStdout = true; // Avvia il processo usando spawn - const monerodProcess = spawn(monerodPath, commandOptions); + monerodProcess = spawn(monerodPath, commandOptions); // Gestisci l'output di stdout in streaming monerodProcess.stdout.on('data', (data) => { @@ -271,11 +293,34 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr console.log(`monerod exited with code: ${code}`); win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`); win?.webContents.send('monero-close', code); + monerodProcess = null; }); return monerodProcess; } +function monitorMonerod(): void { + if (!monerodProcess) { + win?.webContents.send('on-monitor-monerod-error', 'Monerod not running'); + return; + } + + if (!monerodProcess.pid) { + win?.webContents.send('on-monitor-monerod-error', 'Unknown monero pid'); + return; + } + + pidusage(monerodProcess.pid, (error: Error | null, stats: Stats) => { + if (error) { + win?.webContents.send('on-monitor-monerod-error', `${error}`); + return; + } + + win?.webContents.send('on-monitor-monerod', stats); + + }); +} + const downloadFile = (url: string, destinationDir: string, onProgress: (progress: number) => void): Promise => { return new Promise((resolve, reject) => { const request = (url: string) => { @@ -524,14 +569,18 @@ try { ipcMain.handle('is-wifi-connected', async (event) => { isWifiConnected(); - //win?.webContents.send('is-wifi-connected-result', isWifiConnected()); }); ipcMain.handle('get-os-type', (event) => { win?.webContents.send('got-os-type', { platform: os.platform(), arch: os.arch() }); }) + ipcMain.handle('monitor-monerod', (event: IpcMainInvokeEvent) => { + monitorMonerod(); + }); + } catch (e) { // Catch Error + console.error(e); // throw e; } diff --git a/app/package-lock.json b/app/package-lock.json index c128f32..45781a6 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,16 +1,20 @@ { "name": "monerod-gui", - "version": "14.0.1", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "monerod-gui", - "version": "14.0.1", + "version": "0.1.0", "dependencies": { "os": "^0.1.2", + "pidusage": "^3.0.2", "tar": "^7.4.3", "unbzip2-stream": "^1.4.3" + }, + "devDependencies": { + "@types/pidusage": "^2.0.5" } }, "node_modules/@isaacs/cliui": { @@ -49,6 +53,12 @@ "node": ">=14" } }, + "node_modules/@types/pidusage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/pidusage/-/pidusage-2.0.5.tgz", + "integrity": "sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==", + "dev": true + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -339,6 +349,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -353,6 +374,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -636,6 +676,12 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@types/pidusage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/pidusage/-/pidusage-2.0.5.tgz", + "integrity": "sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==", + "dev": true + }, "ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -813,6 +859,14 @@ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, + "pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "requires": { + "safe-buffer": "^5.2.1" + } + }, "rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -821,6 +875,11 @@ "glob": "^10.3.7" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/app/package.json b/app/package.json index a8b0856..0efed54 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,11 @@ "private": true, "dependencies": { "os": "^0.1.2", + "pidusage": "^3.0.2", "tar": "^7.4.3", "unbzip2-stream": "^1.4.3" + }, + "devDependencies": { + "@types/pidusage": "^2.0.5" } } diff --git a/app/preload.js b/app/preload.js index 6bbff45..5590a25 100644 --- a/app/preload.js +++ b/app/preload.js @@ -9,6 +9,15 @@ contextBridge.exposeInMainWorld('electronAPI', { onMonerodStarted: (callback) => { ipcRenderer.on('monerod-started', callback); }, + monitorMonerod: () => { + ipcRenderer.invoke('monitor-monerod'); + }, + onMonitorMonerod: (callback) => { + ipcRenderer.on('on-monitor-monerod', callback); + }, + onMonitorMonerodError: (callback) => { + ipcRenderer.on('on-monitor-monerod-error', callback); + }, unsubscribeOnMonerodStarted: () => { const listeners = ipcRenderer.listeners('monerod-started'); diff --git a/src/app/core/services/daemon/daemon-data.service.ts b/src/app/core/services/daemon/daemon-data.service.ts index 8694192..61920dc 100644 --- a/src/app/core/services/daemon/daemon-data.service.ts +++ b/src/app/core/services/daemon/daemon-data.service.ts @@ -1,6 +1,6 @@ import { EventEmitter, Injectable, NgZone } from '@angular/core'; import { DaemonService } from './daemon.service'; -import { BlockCount, BlockHeader, Chain, Connection, CoreIsBusyError, DaemonInfo, MinerData, MiningStatus, NetStats, NetStatsHistory, PeerInfo, PublicNode, SyncInfo, TxBacklogEntry, TxPool } from '../../../../common'; +import { BlockCount, BlockHeader, Chain, Connection, CoreIsBusyError, DaemonInfo, MinerData, MiningStatus, NetStats, NetStatsHistory, PeerInfo, ProcessStats, PublicNode, SyncInfo, TxBacklogEntry, TxPool } from '../../../../common'; @Injectable({ providedIn: 'root' @@ -16,6 +16,9 @@ export class DaemonDataService { private _daemonRunning: boolean = false; + private _processStats?: ProcessStats; + private _gettingProcessStats: boolean = false; + private _daemonInfo?: DaemonInfo; private _gettingDaemonInfo: boolean = false; @@ -110,6 +113,14 @@ export class DaemonDataService { return this._refreshing; } + public get processStats(): ProcessStats | undefined { + return this._processStats; + } + + public get gettingProcessStats(): boolean { + return this._gettingProcessStats; + } + public get info(): DaemonInfo | undefined { return this._daemonInfo; } @@ -268,10 +279,6 @@ export class DaemonDataService { return Date.now() - this._lastRefresh <= this.refreshTimeoutMs; } - private async getInfo(): Promise { - - } - private async refreshMiningStatus(): Promise { this._gettingMiningStatus = true; @@ -409,7 +416,7 @@ export class DaemonDataService { } this._gettingIsBlockchainPruned = false; - await this.refreshAltChains(); + if (this._daemonInfo.synchronized) await this.refreshAltChains(); this._gettingNetStats = true; this.netStatsRefreshStart.emit(); @@ -418,7 +425,7 @@ export class DaemonDataService { this.netStatsRefreshEnd.emit(); this._gettingNetStats = false; - await this.refreshMiningStatus(); + if (this._daemonInfo.synchronized) await this.refreshMiningStatus(); if (this._daemonInfo.synchronized) await this.refreshMinerData(); @@ -440,6 +447,8 @@ export class DaemonDataService { this._connections = await this.daemonService.getConnections(); this._gettingConnections = false; + await this.refreshProcessStats(); + this._lastRefreshHeight = this._daemonInfo.heightWithoutBootstrap; this._lastRefresh = Date.now(); } catch(error) { @@ -468,4 +477,18 @@ export class DaemonDataService { this._refreshing = false; } + private async refreshProcessStats(): Promise { + this._gettingProcessStats = true; + + try { + this._processStats = await this.daemonService.getProcessStats(); + } + catch(error: any) { + console.error(error); + this._processStats = undefined; + } + + this._gettingProcessStats = false; + } + } diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index 93b3ae0..9151272 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -78,7 +78,7 @@ import { TxInfo } from '../../../../common/TxInfo'; import { DaemonSettings } from '../../../../common/DaemonSettings'; import { MethodNotFoundError } from '../../../../common/error/MethodNotFoundError'; import { openDB, IDBPDatabase } from "idb" -import { PeerInfo, TxPool } from '../../../../common'; +import { PeerInfo, ProcessStats, TxPool } from '../../../../common'; import { MoneroInstallerService } from '../monero-installer/monero-installer.service'; @Injectable({ @@ -1120,6 +1120,26 @@ export class DaemonService { return "0.1.0-alpha"; } + public async getProcessStats(): Promise { + if (!await this.isRunning()) { + throw new Error("Daemon not running"); + } + + const getProcessStatsPromise = new Promise((resolve, reject) => { + window.electronAPI.onMonitorMonerodError((event: any, error: string) => { + reject(error); + }); + + window.electronAPI.onMonitorMonerod((event: any, stats: ProcessStats) => { + resolve(stats); + }); + }) + + window.electronAPI.monitorMonerod(); + + return await getProcessStatsPromise; + } + } export interface RpcError { code: number, message: string } \ No newline at end of file diff --git a/src/app/pages/detail/detail.component.ts b/src/app/pages/detail/detail.component.ts index 865f288..1288604 100644 --- a/src/app/pages/detail/detail.component.ts +++ b/src/app/pages/detail/detail.component.ts @@ -232,7 +232,7 @@ export class DetailComponent implements AfterViewInit, OnDestroy { if (this.daemonData.initializing || this.daemonService.starting) { return this.createLoadingCards(); } - return [ + const cards = [ new Card('Connection Status', this.connectionStatus), new Card('Network Type', this.networkType), new Card('Node Type', this.nodeType), @@ -246,6 +246,15 @@ export class DetailComponent implements AfterViewInit, OnDestroy { new Card('Transaction count', `${this.txCount}`), new Card('Pool size', `${this.poolSize}`) ]; + + if (this.daemonData.processStats) { + cards.push( + new Card('CPU usage', `${this.daemonData.processStats.cpu.toFixed(2)} %`), + new Card('Memory usage', `${(this.daemonData.processStats.memory / 1024 / 1024).toFixed(2)} MB`) + ); + } + + return cards; } public getPeers(): Connection[] { diff --git a/src/common/ProcessStats.ts b/src/common/ProcessStats.ts new file mode 100644 index 0000000..c658082 --- /dev/null +++ b/src/common/ProcessStats.ts @@ -0,0 +1,9 @@ +export interface ProcessStats { + cpu: number; + memory: number; + ppid: number; + pid: number; + ctime: number; + elapsed: number; + timestamp: number; +} diff --git a/src/common/index.ts b/src/common/index.ts index c2b6af4..8f522de 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -38,6 +38,7 @@ export { NetStatsHistory, NetStatsHistoryEntry } from './NetStatsHistory'; export { UnconfirmedTx } from './UnconfirmedTx'; export { SpentKeyImage } from './SpentKeyImage'; export { TxPool } from './TxPool'; +export { ProcessStats } from './ProcessStats'; export * from './error'; export * from './request'; diff --git a/src/polyfills.ts b/src/polyfills.ts index 94d655c..3a76f41 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -59,6 +59,18 @@ declare global { interface Window { electronAPI: { startMonerod: (options: string[]) => void; + monitorMonerod: () => void; + onMonitorMonerod: (callback: (event: any, stats: { + cpu: number; + memory: number; + ppid: number; + pid: number; + ctime: number; + elapsed: number; + timestamp: number; + } + ) => void) => void; + onMonitorMonerodError: (callback: (event: any, error: string) => void) => void; onMonerodStarted: (callback: (event: any, started: boolean) => void) => void; unsubscribeOnMonerodStarted: () => void; onMoneroClose: (callback: (event: any, code: number) => void) => void;