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 * 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<boolean> {
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<string> => {
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;
}

63
app/package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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');

View file

@ -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<void> {
}
private async refreshMiningStatus(): Promise<void> {
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<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 { 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<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 }

View file

@ -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[] {

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 { SpentKeyImage } from './SpentKeyImage';
export { TxPool } from './TxPool';
export { ProcessStats } from './ProcessStats';
export * from './error';
export * from './request';

View file

@ -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;