Monerod process stats

This commit is contained in:
everoddandeven 2024-10-16 18:18:43 +02:00
parent 10d832a614
commit cd2bb56522
10 changed files with 233 additions and 38 deletions

View file

@ -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 { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
@ -6,6 +6,44 @@ import * as https from 'https';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import * as tar from 'tar'; import * as tar from 'tar';
import * as os from 'os'; 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 bz2 from 'unbzip2-stream';
//import * as 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 win: BrowserWindow | null = null;
let isHidden: boolean = false; let isHidden: boolean = false;
let isQuitting: boolean = false; let isQuitting: boolean = false;
let monerodProcess: ChildProcessWithoutNullStreams | null = null;
const args = process.argv.slice(1), const args = process.argv.slice(1),
serve = args.some(val => val === '--serve'); serve = args.some(val => val === '--serve');
@ -133,28 +172,6 @@ function createWindow(): BrowserWindow {
return win; 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<boolean> { function isConnectedToWiFi(): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const platform = os.platform(); // Use os to get the platform const platform = os.platform(); // Use os to get the platform
@ -229,8 +246,13 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
const monerodPath = commandOptions.shift(); const monerodPath = commandOptions.shift();
if (!monerodPath) { if (!monerodPath) {
win?.webContents.send('monero-sterr', `Invalid monerod path provided: ${monerodPath}`); win?.webContents.send('monero-stderr', `Invalid monerod path provided: ${monerodPath}`);
throw Error("Invalid monerod path provided"); 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(" ")); console.log("Starting monerod daemon with options: " + commandOptions.join(" "));
@ -238,7 +260,7 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
moneroFirstStdout = true; moneroFirstStdout = true;
// Avvia il processo usando spawn // Avvia il processo usando spawn
const monerodProcess = spawn(monerodPath, commandOptions); monerodProcess = spawn(monerodPath, commandOptions);
// Gestisci l'output di stdout in streaming // Gestisci l'output di stdout in streaming
monerodProcess.stdout.on('data', (data) => { monerodProcess.stdout.on('data', (data) => {
@ -271,11 +293,34 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
console.log(`monerod exited with code: ${code}`); console.log(`monerod exited with code: ${code}`);
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`); win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
win?.webContents.send('monero-close', code); win?.webContents.send('monero-close', code);
monerodProcess = null;
}); });
return monerodProcess; 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<string> => { const downloadFile = (url: string, destinationDir: string, onProgress: (progress: number) => void): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = (url: string) => { const request = (url: string) => {
@ -524,14 +569,18 @@ try {
ipcMain.handle('is-wifi-connected', async (event) => { ipcMain.handle('is-wifi-connected', async (event) => {
isWifiConnected(); isWifiConnected();
//win?.webContents.send('is-wifi-connected-result', isWifiConnected());
}); });
ipcMain.handle('get-os-type', (event) => { ipcMain.handle('get-os-type', (event) => {
win?.webContents.send('got-os-type', { platform: os.platform(), arch: os.arch() }); win?.webContents.send('got-os-type', { platform: os.platform(), arch: os.arch() });
}) })
ipcMain.handle('monitor-monerod', (event: IpcMainInvokeEvent) => {
monitorMonerod();
});
} catch (e) { } catch (e) {
// Catch Error // Catch Error
console.error(e);
// throw e; // throw e;
} }

63
app/package-lock.json generated
View file

@ -1,16 +1,20 @@
{ {
"name": "monerod-gui", "name": "monerod-gui",
"version": "14.0.1", "version": "0.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "monerod-gui", "name": "monerod-gui",
"version": "14.0.1", "version": "0.1.0",
"dependencies": { "dependencies": {
"os": "^0.1.2", "os": "^0.1.2",
"pidusage": "^3.0.2",
"tar": "^7.4.3", "tar": "^7.4.3",
"unbzip2-stream": "^1.4.3" "unbzip2-stream": "^1.4.3"
},
"devDependencies": {
"@types/pidusage": "^2.0.5"
} }
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@ -49,6 +53,12 @@
"node": ">=14" "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": { "node_modules/ansi-regex": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@ -339,6 +349,17 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/rimraf": {
"version": "5.0.10", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@ -353,6 +374,25 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -636,6 +676,12 @@
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true "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": { "ansi-regex": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "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" "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": { "rimraf": {
"version": "5.0.10", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@ -821,6 +875,11 @@
"glob": "^10.3.7" "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": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View file

@ -11,7 +11,11 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"os": "^0.1.2", "os": "^0.1.2",
"pidusage": "^3.0.2",
"tar": "^7.4.3", "tar": "^7.4.3",
"unbzip2-stream": "^1.4.3" "unbzip2-stream": "^1.4.3"
},
"devDependencies": {
"@types/pidusage": "^2.0.5"
} }
} }

View file

@ -9,6 +9,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
onMonerodStarted: (callback) => { onMonerodStarted: (callback) => {
ipcRenderer.on('monerod-started', 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: () => { unsubscribeOnMonerodStarted: () => {
const listeners = ipcRenderer.listeners('monerod-started'); const listeners = ipcRenderer.listeners('monerod-started');

View file

@ -1,6 +1,6 @@
import { EventEmitter, Injectable, NgZone } from '@angular/core'; import { EventEmitter, Injectable, NgZone } from '@angular/core';
import { DaemonService } from './daemon.service'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -16,6 +16,9 @@ export class DaemonDataService {
private _daemonRunning: boolean = false; private _daemonRunning: boolean = false;
private _processStats?: ProcessStats;
private _gettingProcessStats: boolean = false;
private _daemonInfo?: DaemonInfo; private _daemonInfo?: DaemonInfo;
private _gettingDaemonInfo: boolean = false; private _gettingDaemonInfo: boolean = false;
@ -110,6 +113,14 @@ export class DaemonDataService {
return this._refreshing; return this._refreshing;
} }
public get processStats(): ProcessStats | undefined {
return this._processStats;
}
public get gettingProcessStats(): boolean {
return this._gettingProcessStats;
}
public get info(): DaemonInfo | undefined { public get info(): DaemonInfo | undefined {
return this._daemonInfo; return this._daemonInfo;
} }
@ -268,10 +279,6 @@ export class DaemonDataService {
return Date.now() - this._lastRefresh <= this.refreshTimeoutMs; return Date.now() - this._lastRefresh <= this.refreshTimeoutMs;
} }
private async getInfo(): Promise<void> {
}
private async refreshMiningStatus(): Promise<void> { private async refreshMiningStatus(): Promise<void> {
this._gettingMiningStatus = true; this._gettingMiningStatus = true;
@ -409,7 +416,7 @@ export class DaemonDataService {
} }
this._gettingIsBlockchainPruned = false; this._gettingIsBlockchainPruned = false;
await this.refreshAltChains(); if (this._daemonInfo.synchronized) await this.refreshAltChains();
this._gettingNetStats = true; this._gettingNetStats = true;
this.netStatsRefreshStart.emit(); this.netStatsRefreshStart.emit();
@ -418,7 +425,7 @@ export class DaemonDataService {
this.netStatsRefreshEnd.emit(); this.netStatsRefreshEnd.emit();
this._gettingNetStats = false; this._gettingNetStats = false;
await this.refreshMiningStatus(); if (this._daemonInfo.synchronized) await this.refreshMiningStatus();
if (this._daemonInfo.synchronized) await this.refreshMinerData(); if (this._daemonInfo.synchronized) await this.refreshMinerData();
@ -440,6 +447,8 @@ export class DaemonDataService {
this._connections = await this.daemonService.getConnections(); this._connections = await this.daemonService.getConnections();
this._gettingConnections = false; this._gettingConnections = false;
await this.refreshProcessStats();
this._lastRefreshHeight = this._daemonInfo.heightWithoutBootstrap; this._lastRefreshHeight = this._daemonInfo.heightWithoutBootstrap;
this._lastRefresh = Date.now(); this._lastRefresh = Date.now();
} catch(error) { } catch(error) {
@ -468,4 +477,18 @@ export class DaemonDataService {
this._refreshing = false; this._refreshing = false;
} }
private async refreshProcessStats(): Promise<void> {
this._gettingProcessStats = true;
try {
this._processStats = await this.daemonService.getProcessStats();
}
catch(error: any) {
console.error(error);
this._processStats = undefined;
}
this._gettingProcessStats = false;
}
} }

View file

@ -78,7 +78,7 @@ import { TxInfo } from '../../../../common/TxInfo';
import { DaemonSettings } from '../../../../common/DaemonSettings'; import { DaemonSettings } from '../../../../common/DaemonSettings';
import { MethodNotFoundError } from '../../../../common/error/MethodNotFoundError'; import { MethodNotFoundError } from '../../../../common/error/MethodNotFoundError';
import { openDB, IDBPDatabase } from "idb" import { openDB, IDBPDatabase } from "idb"
import { PeerInfo, TxPool } from '../../../../common'; import { PeerInfo, ProcessStats, TxPool } from '../../../../common';
import { MoneroInstallerService } from '../monero-installer/monero-installer.service'; import { MoneroInstallerService } from '../monero-installer/monero-installer.service';
@Injectable({ @Injectable({
@ -1120,6 +1120,26 @@ export class DaemonService {
return "0.1.0-alpha"; return "0.1.0-alpha";
} }
public async getProcessStats(): Promise<ProcessStats> {
if (!await this.isRunning()) {
throw new Error("Daemon not running");
}
const getProcessStatsPromise = new Promise<ProcessStats>((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 } export interface RpcError { code: number, message: string }

View file

@ -232,7 +232,7 @@ export class DetailComponent implements AfterViewInit, OnDestroy {
if (this.daemonData.initializing || this.daemonService.starting) { if (this.daemonData.initializing || this.daemonService.starting) {
return this.createLoadingCards(); return this.createLoadingCards();
} }
return [ const cards = [
new Card('Connection Status', this.connectionStatus), new Card('Connection Status', this.connectionStatus),
new Card('Network Type', this.networkType), new Card('Network Type', this.networkType),
new Card('Node Type', this.nodeType), new Card('Node Type', this.nodeType),
@ -246,6 +246,15 @@ export class DetailComponent implements AfterViewInit, OnDestroy {
new Card('Transaction count', `${this.txCount}`), new Card('Transaction count', `${this.txCount}`),
new Card('Pool size', `${this.poolSize}`) 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[] { public getPeers(): Connection[] {

View file

@ -0,0 +1,9 @@
export interface ProcessStats {
cpu: number;
memory: number;
ppid: number;
pid: number;
ctime: number;
elapsed: number;
timestamp: number;
}

View file

@ -38,6 +38,7 @@ export { NetStatsHistory, NetStatsHistoryEntry } from './NetStatsHistory';
export { UnconfirmedTx } from './UnconfirmedTx'; export { UnconfirmedTx } from './UnconfirmedTx';
export { SpentKeyImage } from './SpentKeyImage'; export { SpentKeyImage } from './SpentKeyImage';
export { TxPool } from './TxPool'; export { TxPool } from './TxPool';
export { ProcessStats } from './ProcessStats';
export * from './error'; export * from './error';
export * from './request'; export * from './request';

View file

@ -59,6 +59,18 @@ declare global {
interface Window { interface Window {
electronAPI: { electronAPI: {
startMonerod: (options: string[]) => void; 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; onMonerodStarted: (callback: (event: any, started: boolean) => void) => void;
unsubscribeOnMonerodStarted: () => void; unsubscribeOnMonerodStarted: () => void;
onMoneroClose: (callback: (event: any, code: number) => void) => void; onMoneroClose: (callback: (event: any, code: number) => void) => void;