diff --git a/app/main.ts b/app/main.ts index 5e7d9a1..b2de820 100644 --- a/app/main.ts +++ b/app/main.ts @@ -8,9 +8,10 @@ import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstr import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { AppMainProcess, I2pdProcess, MonerodProcess, PrivateTestnet } from './process'; import { BatteryUtils, FileUtils, NetworkUtils } from './utils'; +import { MoneroI2pdProcess } from './process/I2pdProcess'; app.setName('Monero Daemon'); @@ -395,6 +396,13 @@ async function monitorMonerod(): Promise { // #region Download Utils +async function detectInstallation(program: string): Promise { + if (program === 'i2pd') { + return await I2pdProcess.detectInstalled(); + } + else return undefined; +} + async function downloadAndVerifyHash(hashUrl: string, fileName: string, filePath: string): Promise { const hashFileName = await FileUtils.downloadFile(hashUrl, app.getPath('temp'), () => {}); const hashFilePath = `${app.getPath('temp')}/${hashFileName}`; @@ -529,8 +537,14 @@ try { // #endregion - ipcMain.handle('start-i2pd', async (event: IpcMainInvokeEvent, params: { eventId: string; path: string, flags: string[] }) => { - const { eventId, path, flags } = params; + ipcMain.handle('detect-installation', async (event: IpcMainInvokeEvent, params: { eventId: string; program: string }) => { + const { eventId, program } = params; + + win?.webContents.send(eventId, await detectInstallation(program)); + }); + + ipcMain.handle('start-i2pd', async (event: IpcMainInvokeEvent, params: { eventId: string; path: string }) => { + const { eventId, path } = params; let error: string | undefined = undefined; @@ -542,7 +556,8 @@ try { } else { try { - i2pdProcess = new I2pdProcess({ i2pdPath: path, flags, isExe: true }); + //i2pdProcess = new I2pdProcess({ i2pdPath: path, flags, isExe: true }); + i2pdProcess = MoneroI2pdProcess.createSimple(path); await i2pdProcess.start(); i2pdProcess.onStdOut((out: string) => win?.webContents.send('on-ip2d-stdout', out)); i2pdProcess.onStdErr((out: string) => win?.webContents.send('on-ip2d-stderr', out)); @@ -578,7 +593,11 @@ try { ipcMain.handle('is-on-battery-power', async (event: IpcMainInvokeEvent, params: { eventId: string; }) => { const onBattery = await BatteryUtils.isOnBatteryPower(); - win?.webContents.send(params.eventId, onBattery); + if (!win) { + console.warn("is-on-battery-power: window not set"); + return; + } + win.webContents.send(params.eventId, onBattery); }); powerMonitor.on('on-ac', () => win?.webContents.send('on-ac')); @@ -603,6 +622,8 @@ try { } else await monerodProcess.stop(); monerodProcess = null; + + if (i2pdProcess && i2pdProcess.running) await i2pdProcess.stop(); } } catch (error: any) { @@ -903,10 +924,16 @@ try { win?.webContents.send(eventId, { data: result.data, code: result.status, status: result.statusText }); } catch (error: any) { - console.error("post(): ", error); - win?.webContents.send(eventId, { error: `${error}` }); - } + let msg: string; + if (error instanceof AxiosError) { + msg = error.message + } + else msg = `${error}`; + + console.error(msg); + win?.webContents.send(eventId, { error: msg }); + } }); ipcMain.handle('http-get', async (event: IpcMainInvokeEvent, params: { id: string; url: string; config?: AxiosRequestConfig }) => { @@ -918,8 +945,15 @@ try { win?.webContents.send(eventId, { data: result.data, code: result.status, status: result.statusText }); } catch (error: any) { - console.error("get(): ", error); - win?.webContents.send(eventId, { error: `${error}` }); + let msg: string; + if (error instanceof AxiosError) { + msg = error.message + } + else msg = `${error}`; + + console.error(msg); + + win?.webContents.send(eventId, { error: msg }); } }); diff --git a/app/preload.js b/app/preload.js index c9d70d2..98db43a 100644 --- a/app/preload.js +++ b/app/preload.js @@ -6,35 +6,42 @@ function newId() { } contextBridge.exposeInMainWorld('electronAPI', { + detectInstallation: (program, callback) => { + const eventId = `on-detect-installation-${newId()}`; + + const handler = (event, result) => { + callback(result); + }; + + ipcRenderer.once(eventId, handler); + ipcRenderer.invoke('detect-installation', { eventId, program }); + }, checkValidI2pdPath: (path, callback) => { const eventId = `on-check-valid-i2pd-path-${newId()}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('check-valid-i2pd-path', { eventId, path }); }, - startI2pd: (path, flags, callback) => { + startI2pd: (path, callback) => { const eventId = `on-start-i2pd-${newId()}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); - ipcRenderer.invoke('start-i2pd', { eventId, path, flags }); + ipcRenderer.once(eventId, handler); + ipcRenderer.invoke('start-i2pd', { eventId, path }); }, stopI2pd: (callback) => { const eventId = `on-stop-i2pd-${newId()}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('stop-i2pd', { eventId }); }, onI2pdOutput: (callback) => { @@ -59,11 +66,10 @@ contextBridge.exposeInMainWorld('electronAPI', { const eventId = `on-http-post-result-${id}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('http-post', params); }, httpGet: (params, callback) => { @@ -73,30 +79,28 @@ contextBridge.exposeInMainWorld('electronAPI', { const eventId = `on-http-get-result-${id}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('http-get', params); }, getBatteryLevel: (callback) => { - const eventId = `on-get-battery-level-${newId()};` + const eventId = `on-get-battery-level-${newId()}`; const handler = (event, result) => { - ipcRenderer.off(eventId, handler); callback(result); }; - ipcRenderer.on(eventId, handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('get-battery-level', { eventId }); }, isOnBatteryPower: (callback) => { - const eventId = `on-is-on-battery-power-${newId()};` + //const eventId = `on-is-on-battery-power-${newId()}`; + const eventId = 'on-is-on-battery-power'; const handler = (event, result) => { - ipcRenderer.off(eventId); callback(result); }; - ipcRenderer.on('on-is-on-battery-power', handler); + ipcRenderer.once(eventId, handler); ipcRenderer.invoke('is-on-battery-power', { eventId }); }, onAc: (callback) => { @@ -272,11 +276,10 @@ contextBridge.exposeInMainWorld('electronAPI', { }, quit: (callback) => { const handler = (event, result) => { - ipcRenderer.off('on-quit', handler); callback(result); }; - ipcRenderer.on('on-quit', handler); + ipcRenderer.once('on-quit', handler); ipcRenderer.invoke('quit'); }, enableAutoLaunch: (minimized) => { diff --git a/app/process/AppChildProcess.ts b/app/process/AppChildProcess.ts index 8e7c3be..5db08a6 100644 --- a/app/process/AppChildProcess.ts +++ b/app/process/AppChildProcess.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import * as os from 'os'; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { ProcessStats } from './ProcessStats'; @@ -7,263 +8,278 @@ const pidusage = require('pidusage'); export class AppChildProcess { - protected _starting: boolean = false; - protected _stopping: boolean = false; - protected _running: boolean = false; - protected _isExe: boolean = true; + public static get platform(): NodeJS.Platform { + return os.platform(); + } - protected _process?: ChildProcessWithoutNullStreams; - - protected _command: string; - protected readonly _args?: string[]; - - public get pid(): number { - if (!this._process || this._process.pid == null) { - return -1; - } + public static get isLinux(): boolean { + return this.platform === 'linux'; + } - return this._process.pid; - } + public static get isWindows(): boolean { + return this.platform === 'win32'; + } - protected readonly _handlers: { - 'stdout': ((data: string) => void)[], - 'stderr': ((err: string) => void)[], - 'onclose': ((code: number | null) => void)[], - 'onerror': ((error: Error) => void)[], - } = { - 'stdout': [], - 'stderr': [], - 'onclose': [], - 'onerror': [] - }; - - private readonly mOnErrorDefaultHandler: (error: Error) => void = (error: Error) => { - if (!this._process) { - return; - } + public static get isMacos(): boolean { + return this.platform === 'darwin'; + } - const listeners = this._process.listeners('error'); - - if (listeners.length > 1) { - return; - } + protected _starting: boolean = false; + protected _stopping: boolean = false; + protected _running: boolean = false; + protected _isExe: boolean = true; - console.error("Uncaught exeception: "); - console.error(error); - }; + protected _process?: ChildProcessWithoutNullStreams; + + protected _command: string; + protected readonly _args?: string[]; - public get command(): string { - return this._command; + public get pid(): number { + if (!this._process || this._process.pid == null) { + return -1; } - public get args(): string[] { - return this._args ? this._args : []; + return this._process.pid; + } + + protected readonly _handlers: { + 'stdout': ((data: string) => void)[], + 'stderr': ((err: string) => void)[], + 'onclose': ((code: number | null) => void)[], + 'onerror': ((error: Error) => void)[], + } = { + 'stdout': [], + 'stderr': [], + 'onclose': [], + 'onerror': [] + }; + + private readonly mOnErrorDefaultHandler: (error: Error) => void = (error: Error) => { + if (!this._process) { + return; } - public get running(): boolean { - return this._running; + const listeners = this._process.listeners('error'); + + if (listeners.length > 1) { + return; } - public get stopping(): boolean { - return this._stopping; - } + console.error("Uncaught exeception: "); + console.error(error); + }; - public get starting(): boolean { - return this._starting; - } + public get command(): string { + return this._command; + } - constructor({ command, args, isExe } : { command: string, args?: string[], isExe?: boolean}) { - this._command = command; - this._args = args; - this._isExe = isExe === false ? false : true; - } + public get args(): string[] { + return this._args ? this._args : []; + } - protected static replaceAll(value: string, oldValue: string, newValue: string): string { - let v = value; + public get running(): boolean { + return this._running; + } - while(v.includes(oldValue)) { - v = v.replace(oldValue, newValue); - } + public get stopping(): boolean { + return this._stopping; + } - return v; + public get starting(): boolean { + return this._starting; + } + + constructor({ command, args, isExe } : { command: string, args?: string[], isExe?: boolean}) { + this._command = command; + this._args = args; + this._isExe = isExe === false ? false : true; + } + + protected static replaceAll(value: string, oldValue: string, newValue: string): string { + let v = value; + + while(v.includes(oldValue)) { + v = v.replace(oldValue, newValue); } - protected static checkExecutable(executablePath: string): void { - const exeComponents: string[] = executablePath.split(" ").filter((c) => c != ''); - console.log("AppProcess.checkExecutable(): " + executablePath); - if (exeComponents.length == 0) { - throw new Error("Invalid command provided"); - } + return v; + } - const exePath = exeComponents[0]; + protected static checkExecutable(executablePath: string): void { + const exeComponents: string[] = executablePath.split(" ").filter((c) => c != ''); + console.log("AppProcess.checkExecutable(): " + executablePath); + if (exeComponents.length == 0) { + throw new Error("Invalid command provided"); + } - if (!fs.existsSync(exePath)) { - throw new Error("Cannot find executable: " + exePath); - } + const exePath = exeComponents[0]; + + if (!fs.existsSync(exePath)) { + throw new Error("Cannot find executable: " + exePath); } - - protected checkExecutable(): void { - AppChildProcess.checkExecutable(this.command); + } + + protected checkExecutable(): void { + AppChildProcess.checkExecutable(this.command); + } + + public onStdOut(callback: (out: string) => void): void { + const cbk = (chunk: any) => callback(`${chunk}`); + + if (!this._process) { + this._handlers.stdout.push(cbk); + return; } - public onStdOut(callback: (out: string) => void): void { - const cbk = (chunk: any) => callback(`${chunk}`); + this._process.stdout.on('data', cbk); + } - if (!this._process) { - this._handlers.stdout.push(cbk); - return; - } + public onStdErr(callback: (err: string) => void): void { + const cbk = (chunk: any) => callback(`${chunk}`); - this._process.stdout.on('data', cbk); + if (!this._process) { + this._handlers.stderr.push(cbk); + return; } - public onStdErr(callback: (err: string) => void): void { - const cbk = (chunk: any) => callback(`${chunk}`); + this._process.stderr.on('data', cbk); + } - if (!this._process) { - this._handlers.stderr.push(cbk); - return; - } - - this._process.stderr.on('data', cbk); + public onError(callback: (err: Error) => void): void { + if (!this._process) + { + this._handlers.onerror.push(callback); + return; } - public onError(callback: (err: Error) => void): void { - if (!this._process) - { - this._handlers.onerror.push(callback); - return; - } + this._process.on('error', callback); + } - this._process.on('error', callback); + public onClose(callback: (code: number | null) => void): void { + if (!this._process) { + this._handlers.onclose.push(callback); + return; } - public onClose(callback: (code: number | null) => void): void { - if (!this._process) { - this._handlers.onclose.push(callback); - return; - } + this._process.on('close', callback); + } - this._process.on('close', callback); + public async start(): Promise { + if (this._starting) { + throw new Error("Process is already starting"); } - public async start(): Promise { - if (this._starting) { - throw new Error("Process is already starting"); - } + if (this._stopping) { + throw new Error("Process is stopping"); + } - if (this._stopping) { - throw new Error("Process is stopping"); - } + if (this._running) { + throw new Error("Process already running"); + } - if (this._running) { - throw new Error("Process already running"); - } + if (this._isExe) { + this.checkExecutable(); + } - if (this._isExe) { - this.checkExecutable(); - } + this._starting = true; - this._starting = true; + const process = spawn(this._command, this._args); + this._process = process; - const process = spawn(this._command, this._args); - this._process = process; + const promise = new Promise((resolve, reject) => { + const onSpawnError = (err: Error) => { + this._starting = false; + this._running = false; + reject(err); + }; - const promise = new Promise((resolve, reject) => { - const onSpawnError = (err: Error) => { - this._starting = false; - this._running = false; - reject(err); - }; + const onSpawn = () => { + this._starting = false; + this._running = true; + process.on('error', this.mOnErrorDefaultHandler); - const onSpawn = () => { - this._starting = false; - this._running = true; - process.off('error', onSpawnError); - process.on('error', this.mOnErrorDefaultHandler); + this._handlers.onclose.forEach((listener) => process.on('close', listener)); + this._handlers.onerror.forEach((listener) => process.on('error', listener)); + this._handlers.stdout.forEach((listener) => process.stdout.on('data', listener)); + this._handlers.stderr.forEach((listener) => process.stderr.on('data', listener)); + + resolve(); + }; - this._handlers.onclose.forEach((listener) => process.on('close', listener)); - this._handlers.onerror.forEach((listener) => process.on('error', listener)); - this._handlers.stdout.forEach((listener) => process.stdout.on('data', listener)); - this._handlers.stderr.forEach((listener) => process.stderr.on('data', listener)); - - resolve(); - }; + process.once('error', onSpawnError); + process.once('spawn', onSpawn); + }); - process.once('error', onSpawnError); - process.once('spawn', onSpawn); - }); + process.once('close', () => { + if (this._stopping) return; - process.once('close', () => { - if (this._stopping) return; + this._running = false; + this._process = undefined; + }); - this._running = false; - this._process = undefined; - }); + await promise; + } - await promise; + public async stop(): Promise { + if (this._starting) { + throw new Error("Process is starting"); } - public async stop(): Promise { - if (this._starting) { - throw new Error("Process is starting"); - } + if (this._stopping) { + throw new Error("Process is already stopping"); + } - if (this._stopping) { - throw new Error("Process is already stopping"); - } + const proc = this._process; - const proc = this._process; + if (!this._running || !proc) { + throw new Error("Process is not running"); + } - if (!this._running || !proc) { - throw new Error("Process is not running"); - } + this._stopping = true; + + const promise = new Promise((resolve, reject) => { + proc.on('close', (code: number | null) => { + this._process = undefined; + this._running = false; + this._stopping = false; + resolve(code); + }); + proc.on('exit', (code: | number) => { + this._process = undefined; + this._running = false; + this._stopping = false; + resolve(code); + }); + }); + + if (!proc.kill()) { + throw new Error("Could not kill monerod process: " + proc.pid); + } - this._stopping = true; - - const promise = new Promise((resolve, reject) => { - proc.on('close', (code: number | null) => { - this._process = undefined; - this._running = false; - this._stopping = false; - resolve(code); - }); - proc.on('exit', (code: | number) => { - this._process = undefined; - this._running = false; - this._stopping = false; - resolve(code); - }); - }); - - if (!proc.kill()) { - throw new Error("Could not kill monerod process: " + proc.pid); - } + return await promise; + } - return await promise; + public async getStats(): Promise { + if (!this._process) { + throw new Error("Process not running"); } - public async getStats(): Promise { - if (!this._process) { - throw new Error("Process not running"); - } + const pid = this._process.pid; + + if (!pid) { + throw new Error("Process is unknown"); + } - const pid = this._process.pid; - - if (!pid) { - throw new Error("Process is unknown"); + return await new Promise((resolve, reject) => { + pidusage(pid, (err: Error | null, stats: ProcessStats) => { + if (err) { + reject(err); } - - return await new Promise((resolve, reject) => { - pidusage(pid, (err: Error | null, stats: ProcessStats) => { - if (err) { - reject(err); - } - else { - resolve(stats); - } - }); - }); - } + else { + resolve(stats); + } + }); + }); + } } \ No newline at end of file diff --git a/app/process/I2pdProcess.ts b/app/process/I2pdProcess.ts index 943f4cc..9c815bc 100644 --- a/app/process/I2pdProcess.ts +++ b/app/process/I2pdProcess.ts @@ -2,52 +2,518 @@ import { execSync } from "child_process"; import { AppChildProcess } from "./AppChildProcess"; import * as fs from 'fs'; -export class I2pdProcess extends AppChildProcess { +export interface InstallationInfo { path: string }; +export interface I2pdInstallationInfo extends InstallationInfo { configFile?: string; tunnelConfig?: string; tunnelsConfigDir?: string; pidFile?: string; isRunning?: boolean; }; + +export interface I2pTunnelConfig { + name: string; + type: 'server' | 'client'; + host: string; + port: number; + inport?: number; + keys: string; +} + +export abstract class I2pTunnelConfigCreator { + public static get emptyConfig(): I2pTunnelConfig { + return { + name: '', + type: 'server', + host: '', + port: 0, + inport: 0, + keys: '' + }; + } +} + +export abstract class I2pTunnelConfigValidator { - constructor({ i2pdPath, flags, isExe }: { i2pdPath: string, flags?: string[], isExe?: boolean }) { - super({ - command: i2pdPath, - args: flags, - isExe: isExe - }); + public static validate(config: I2pTunnelConfig): void { + const { name, type, host, port, inport, keys } = config; + + if (name === '') { + throw new Error("Empty I2P tunnel config name"); + } + if (type !== 'server' && type !== 'client') { + throw new Error("Invalid I2P tunnel config type: " + type); + } + if (host == '') { + throw new Error("Empty I2P tunnel config host"); + } + if (port <= 0) { + throw new Error("Invalid I2P tunnel config port: " + port); } + if (inport !== undefined && inport < 0) { + throw new Error("Invalid I2P tunnel config inport: " + inport); + } + if (keys === '') { + throw new Error("Empty I2P tunnel config keys"); + } + } - public override async start(): Promise { - let message: string = "Starting i2pd process"; + public static isValid(config: I2pTunnelConfig): boolean { + try { + this.validate(config); + return true; + } + catch (error: any) { + console.error(error); + return false; + } + } +} - message += `\n\t${this._isExe ? 'Path' : 'Command'}: ${this._command}`; +export abstract class I2pTunnelConfigConverter { + + public static toString(...configs: I2pTunnelConfig[]): string { + let result = ''; - if (this._args) { - message += `\n\tFlags: ${this._args.join(" ")}` - } + configs.forEach((config, index) => { + const configStr = config.inport !== undefined ? `[${config.name}] + type = ${config.type} + host = ${config.host} + port = ${config.port} + inport = ${config.inport} + keys = ${config.keys}` + : + `[${config.name}] + type = ${config.type} + host = ${config.host} + port = ${config.port} + keys = ${config.keys}` + ; - await super.start(); + if (index > 0) { + result = `${result} + + ${configStr} + `; } - static async isValidPath(executablePath: string): Promise { - // Verifica se il file esiste - if (!fs.existsSync(executablePath)) { - return false; + else result = configStr; + }); + + return result; + } + +} + +export abstract class I2pTunnelConfigParser { + + public static fromString(configStr: string): I2pTunnelConfig[] { + const result: I2pTunnelConfig[] = []; + const lines = configStr.split('\n'); + let firstConfig: boolean = true; + let currentConfig: I2pTunnelConfig | undefined = undefined; + + for (const line of lines) { + const trimmed = line.trim(); + + if (!trimmed || trimmed.startsWith('#')) continue; // Skip empty lines and comments + + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + if (firstConfig && currentConfig !== undefined) { + firstConfig = false; + } + if (currentConfig !== undefined) { + I2pTunnelConfigValidator.validate(currentConfig); + result.push(currentConfig); } - // Verifica se il file è un eseguibile (su Linux) - try { - const stats = fs.statSync(executablePath); - if (!stats.isFile()) { - return false; - } + currentConfig = I2pTunnelConfigCreator.emptyConfig; + currentConfig.name = trimmed.slice(1, -1); + + } else if (currentConfig) { + const [key, ...valueParts] = trimmed.split('='); + const keyTrimmed = key.trim(); + const valueTrimmed = valueParts.join('=').trim(); + + let value: string | number = valueTrimmed; + + if (!isNaN(Number(valueTrimmed))) { + value = Number(valueTrimmed); + } - // Prova a eseguire una versione del comando per ottenere l'output - const output = execSync(`${executablePath} --version`).toString(); - if (output.includes("i2pd")) { - return true; - } - } catch (err) { - return false; + if (keyTrimmed === 'name') { + currentConfig.name = valueTrimmed; + } + else if (keyTrimmed === 'type') { + currentConfig.type = valueTrimmed as 'server' | 'client'; + } + else if (keyTrimmed === 'host') { + currentConfig.host = valueTrimmed; + } + else if (keyTrimmed === 'port') { + currentConfig.port = value as number; + } + else if (keyTrimmed === 'inport') { + currentConfig.inport = value as number; } + else if (keyTrimmed === 'keys') { + currentConfig.keys = valueTrimmed; + } + else throw new Error("Invalid I2P tunnel config key: " + keyTrimmed); + } + } + + if (currentConfig && I2pTunnelConfigValidator.isValid(currentConfig) && !result.includes(currentConfig)) { + result.push(currentConfig); + } + + return result; + } +} + +export abstract class I2pTunnelConfigWriter { + + public static write(config: I2pTunnelConfig | I2pTunnelConfig[], path: string): void { + const configs = (Array.isArray(config)) ? config as I2pTunnelConfig[] : [config as I2pTunnelConfig]; + + configs.forEach((c) => { + if (!I2pTunnelConfigValidator.isValid(c)) { + throw new Error("Invalid I2P config provided: " + c.name); + } + }); + + fs.writeFileSync(path, I2pTunnelConfigConverter.toString(...configs)); + } +} + +export abstract class I2pTunnelConfigReader { + + public static read(path: string): I2pTunnelConfig[] { + if (!fs.existsSync(path)) { + throw new Error("Path doesn't exits: " + path); + } + + const content = fs.readFileSync(path, 'utf-8'); - return false; + return I2pTunnelConfigParser.fromString(content); + } + + public static isValid(path: string): boolean { + try { + this.read(path); + return true; + } + catch { + return false; } + } + +} + +export abstract class I2pTunnelConfigComparator { + public static equals(config1: I2pTunnelConfig, config2: I2pTunnelConfig): boolean { + return I2pTunnelConfigConverter.toString(config1) === I2pTunnelConfigConverter.toString(config2); + } +} + +export abstract class MoneroI2pTunnelConfigReader extends I2pTunnelConfigReader { + + public static override read(path: string): [I2pTunnelConfig, I2pTunnelConfig] { + const result = super.read(path); + + const node = result.find((r) => r.name === 'monero-node'); + const rpc = result.find((r) => r.name === 'monero-rpc'); + + if (!node) throw new Error("Could not find monero-node I2P tunnel configuration"); + if (!rpc) throw new Error("Could not find monero-rpc I2P tunnel configuration"); + + + return [node, rpc]; + } +} + +export abstract class MoneroI2pTunnelConfigCreator { + + public static create({ host, keys, port, rpcPort }: { host: string; keys: string; port: number; rpcPort: number; }): [I2pTunnelConfig, I2pTunnelConfig] { + const inport = 0; + const type = 'server'; + + const node: I2pTunnelConfig = { + name: 'monero-node', + type, + host, + keys, + port, + inport + }; + + const rpc: I2pTunnelConfig = { + name: 'monero-rpc', + type, + host, + keys, + port: rpcPort + }; + + return [ node, rpc ]; + } + + private static getDefaultNodePort(networkType: 'mainnet' | 'stagenet' | 'testnet'): number { + if (networkType === 'mainnet') return 18085; + else if (networkType === 'stagenet') return 38085; + return 28085; + } + + private static getDefaultRpcPort(networkType: 'mainnet' | 'stagenet' | 'testnet'): number { + if (networkType === 'mainnet') return 18089; + else if (networkType === 'stagenet') return 38089; + return 28089; + } + + public static createSimple(networkType: 'mainnet' | 'stagenet' | 'testnet' = 'mainnet', port?: number, rpcPort?: number): [I2pTunnelConfig, I2pTunnelConfig] { + port = port || this.getDefaultNodePort(networkType); + rpcPort = rpcPort || this.getDefaultRpcPort(networkType); + return this.create({ host: '127.0.0.1', keys: `monero-${networkType}.dat`, port, rpcPort }) + } + +} + +export abstract class MoneroI2pTunnelConfigService { + private _loading: boolean = false; + private _saving: boolean = false; + private _loaded: boolean = false; + private _originalConfig: [I2pTunnelConfig, I2pTunnelConfig] = MoneroI2pTunnelConfigCreator.createSimple(); + private _config: [I2pTunnelConfig, I2pTunnelConfig] = MoneroI2pTunnelConfigCreator.createSimple(); + private _path: string = ''; + + public get loaded(): boolean { + return this._loaded; + } + + public get loading(): boolean { + return this._loading; + } + + public get saving(): boolean { + return this._saving; + } + + public get node(): I2pTunnelConfig { + return this._config[0]; + } + + public get rpc(): I2pTunnelConfig { + return this._config[1]; + } + + public get path(): string { + return this._path; + } + + public get modified(): boolean { + return !I2pTunnelConfigComparator.equals(this._originalConfig[0], this._config[0]) || !I2pTunnelConfigComparator.equals(this._originalConfig[1], this._config[1]); + } + + constructor(path: string) { + this._path = path; + } + + public load(_path?: string): void { + const path = _path || this._path; + if (path === '') throw new Error("No path provided"); + if (this._loading) throw new Error("wait for last load"); + if (this._saving) throw new Error("wait for save"); + + let err: any = null; + this._loading = true; + + try { + this._config = MoneroI2pTunnelConfigReader.read(path); + this._originalConfig = [ { ...this._config[0] }, { ...this._config[1] }]; + this._loaded = true; + } + catch(error: any) { + err = error; + } + + this._loading = false; + if (err) throw err; + } + + public save(_path?: string): void { + const path = _path || this._path; + if (path == '') throw new Error("No path provided"); + if (this._loading) throw new Error("wait for last load"); + if (this._saving) throw new Error("already saving"); + + let err: any = null; + this._saving = true; + + try { + if (!this.modified) throw new Error("Config not modified"); + + I2pTunnelConfigWriter.write(this._config, path); + this._originalConfig = [ { ...this._config[0] }, { ...this._config[1] }]; + this._path = path; + this._loaded = true; + } + catch(error: any) { + err = error; + } + + this._saving = false; + if (err) throw err; + } + + public setConfig(node: I2pTunnelConfig, rpc: I2pTunnelConfig, save: boolean = false): void { + if (!I2pTunnelConfigValidator.isValid(node)) throw new Error("Invalid monero node i2p tunnel configuration provided"); + if (!I2pTunnelConfigValidator.isValid(rpc)) throw new Error("Invalid monero rpc i2p tunnel configuration provided"); + + this._config = [node, rpc]; + this._loaded = true; + + if (save) this.save(); + } + +} + +export class I2pdProcess extends AppChildProcess { + + constructor({ i2pdPath, flags, isExe }: { i2pdPath: string, flags?: string[], isExe?: boolean }) { + console.log("Creating I2pdProcess"); + super({ + command: i2pdPath, + args: flags, + isExe: isExe + }); + } + + public override async start(): Promise { + let message: string = "Starting i2pd process"; + + message += `\n\t${this._isExe ? 'Path' : 'Command'}: ${this._command}`; + + if (this._args) { + message += `\n\tFlags: ${this._args.join(" ")}` + } + + console.log(message); + + const promise = new Promise((resolve, reject) => { + let stdOutFound = false; + + const onStdOut = (out: string) => { + stdOutFound = true; + resolve(); + }; + + const onStdErr = (out: string) => { + if (!stdOutFound) reject(new Error(out)); + }; + + this.onError((err) => onStdErr(err.message)); + this.onStdOut(onStdOut); + this.onStdErr(onStdErr); + }); + + await super.start(); + + await promise; + } + + static async isValidPath(executablePath: string): Promise { + // Verifica se il file esiste + if (!fs.existsSync(executablePath)) { + return false; + } + + // Verifica se il file è un eseguibile (su Linux) + try { + const stats = fs.statSync(executablePath); + if (!stats.isFile()) { + return false; + } + + // Prova a eseguire una versione del comando per ottenere l'output + const output = execSync(`${executablePath} --version`).toString(); + if (output.includes("i2pd")) { + return true; + } + } catch (err) { + return false; + } + + return false; + } + + public static async detectInstalled(): Promise { + if (this.isLinux) { + return await this.detectInstalledLinux(); + } + else if (this.isWindows) { + return await this.detectInstalledWindows(); + } + else if (this.isMacos) { + return await this.detectInstalledMacos(); + } + + return undefined; + } + + private static async detectInstalledLinux(): Promise { + let path: string | undefined = undefined; + let configFile: string | undefined = undefined; + let tunnelConfig: string | undefined = undefined; + let tunnelsConfigDir: string | undefined = undefined; + let pidFile: string | undefined = undefined; + let isRunning: boolean = false; + + if (await this.isValidPath('/usr/bin/i2pd')) { + path = '/usr/bin/i2pd'; + } + if (fs.existsSync('/etc/i2pd/i2pd.conf')) { + configFile = '/etc/i2pd/i2pd.conf'; + } + if (fs.existsSync('/etc/i2pd/tunnels.conf')) { + tunnelConfig = '/etc/i2pd/tunnels.conf'; + } + if (fs.existsSync('/etc/i2pd/tunnels.conf.d')) { + tunnelsConfigDir = '/etc/i2pd/tunnels.conf.d' + } + if (fs.existsSync('/run/i2pd/i2pd.pid')) { + pidFile = '/run/i2pd/i2pd.pid'; + isRunning = true; + } + + if (path) { + return { path, configFile, tunnelConfig, tunnelsConfigDir, pidFile, isRunning }; + } + + return undefined; + } + + private static async detectInstalledWindows(): Promise { + return undefined; + } + + private static async detectInstalledMacos(): Promise { + return undefined; + } + +} + +export interface MoneroI2pdProcessOptions { + +}; + +export class MoneroI2pdProcess extends I2pdProcess { + + public static readonly defaultFlags: string[] = [ + '--conf=/etc/i2pd/i2pd.conf', + '--tunconf=/etc/i2pd/tunnels.conf', + '--tunnelsdir=/etc/i2pd/tunnels.conf.d', + //'--pidfile=/run/i2pd/i2pd.pid', + '--logfile=/var/log/i2pd/i2pd.log', + //'--daemon', + //'--service' + ]; + public static createSimple(i2pdPath: string): MoneroI2pdProcess { + return new MoneroI2pdProcess({ i2pdPath, flags: this.defaultFlags, isExe: true }) + } } \ No newline at end of file diff --git a/app/process/MonerodProcess.ts b/app/process/MonerodProcess.ts index de21e5c..766bfb3 100644 --- a/app/process/MonerodProcess.ts +++ b/app/process/MonerodProcess.ts @@ -2,222 +2,222 @@ import { AppChildProcess } from "./AppChildProcess"; export class MonerodProcess extends AppChildProcess { - protected static readonly stdoutPattern: string = '**********************************************************************'; + protected static readonly stdoutPattern: string = '**********************************************************************'; - public privnet: boolean = false; + public privnet: boolean = false; - public get interactive(): boolean { - return this.args ? !this.args.includes('--non-interactive') : true; - } + public get interactive(): boolean { + return this.args ? !this.args.includes('--non-interactive') : true; + } + + public get detached(): boolean { + return this.args ? this.args.includes('--detached') : false; + } + + constructor({ monerodCmd, flags, isExe }: { monerodCmd: string, flags?: string[], isExe?: boolean }) { + super({ + command: monerodCmd, + args: flags, + isExe: isExe + }) + } - public get detached(): boolean { - return this.args ? this.args.includes('--detached') : false; + public static async isValidPath(monerodPath: string): Promise { + console.log(`MonerodProcess.isValidMonerodPath('${monerodPath}')`); + + if (typeof monerodPath !== 'string' || MonerodProcess.replaceAll(monerodPath, " ", "") == "") { + return false; } - constructor({ monerodCmd, flags, isExe }: { monerodCmd: string, flags?: string[], isExe?: boolean }) { - super({ - command: monerodCmd, - args: flags, - isExe: isExe - }) + try { + MonerodProcess.checkExecutable(monerodPath); + } + catch { + return false; } - public static async isValidPath(monerodPath: string): Promise { - console.log(`MonerodProcess.isValidMonerodPath('${monerodPath}')`); + const proc = new AppChildProcess({ + command: monerodPath, + args: [ '--help' ], + isExe: true + }); - if (typeof monerodPath !== 'string' || MonerodProcess.replaceAll(monerodPath, " ", "") == "") { - return false; - } + const promise = new Promise((resolve) => { + let foundUsage: boolean = false; - try { - MonerodProcess.checkExecutable(monerodPath); - } - catch { - return false; - } + proc.onError((err: Error) => { + console.log(`MonerodProcess.isValidMonerodPath(): Error: '${err.message}'`); + resolve(false); + }); - const proc = new AppChildProcess({ - command: monerodPath, - args: [ '--help' ], - isExe: true - }); - - const promise = new Promise((resolve) => { - let foundUsage: boolean = false; - - proc.onError((err: Error) => { - console.log(`MonerodProcess.isValidMonerodPath(): Error: '${err.message}'`); - resolve(false); - }); - - proc.onStdErr((err: string) => { - console.log(`MonerodProcess.isValidMonerodPath(): Std Error: '${err}'`); - resolve(false); - }); - - proc.onStdOut((data: string) => { - if (foundUsage) { - return; - } - - if ( - `${data}`.includes('monerod [options|settings] [daemon_command...]') || - `${data}`.includes('monerod.exe [options|settings] [daemon_command...]') - ) { - foundUsage = true; - } - }); - - proc.onClose((code: number | null) => { - console.log(`MonerodProcess.isValidMonerodPath(): exit code '${code}', found usage: ${foundUsage}`); - resolve(foundUsage && code == 0); - }); - }); - - try { - await proc.start(); + proc.onStdErr((err: string) => { + console.log(`MonerodProcess.isValidMonerodPath(): Std Error: '${err}'`); + resolve(false); + }); + + proc.onStdOut((data: string) => { + if (foundUsage) { + return; } - catch(error: any) { - console.log(`MonerodProcess.isValidMonerodPath(): exit code '${error}'`); + + if ( + `${data}`.includes('monerod [options|settings] [daemon_command...]') || + `${data}`.includes('monerod.exe [options|settings] [daemon_command...]') + ) { + foundUsage = true; } + }); - return await promise; + proc.onClose((code: number | null) => { + console.log(`MonerodProcess.isValidMonerodPath(): exit code '${code}', found usage: ${foundUsage}`); + resolve(foundUsage && code == 0); + }); + }); + + try { + await proc.start(); + } + catch(error: any) { + console.log(`MonerodProcess.isValidMonerodPath(): exit code '${error}'`); } - public override async start(): Promise { - if (this._isExe) { - const validPath = await MonerodProcess.isValidPath(this._command); + return await promise; + } - if (!validPath) { - throw new Error("Invalid monerod path provided: " + this._command); - } - } + public override async start(): Promise { + if (this._isExe) { + const validPath = await MonerodProcess.isValidPath(this._command); - let message: string = "Starting monerod process"; + if (!validPath) { + throw new Error("Invalid monerod path provided: " + this._command); + } + } - message += `\n\t${this._isExe ? 'Path' : 'Command'}: ${this._command}`; + let message: string = "Starting monerod process"; - if (this._args) { - message += `\n\tFlags: ${this._args.join(" ")}` - } + message += `\n\t${this._isExe ? 'Path' : 'Command'}: ${this._command}`; + + if (this._args) { + message += `\n\tFlags: ${this._args.join(" ")}` + } - console.log(message); - - let firstPatternFound = false; - const waitForPattern = this._args ? !this.privnet && !this._args.includes('--version') && !this.args.includes('--help') : true; - - const patternPromise = new Promise((resolve, reject) => { - let firstStdout = true; - let timeout: NodeJS.Timeout | undefined = undefined; - - const onStdOut = (out: string) => { - //console.log(out); - if (firstStdout) { - firstStdout = false; - - if (!waitForPattern) { - return; - } - - timeout = setTimeout(() => { - if (this._process && this._process.exitCode == null) { - this._process.kill(); - } - timeout = undefined; - - reject(new Error("MonerodProcess.start(): Timed out")); - }, 90*1000); - } - - const foundPattern = out.includes(MonerodProcess.stdoutPattern); - - if (!foundPattern) { - return; - } - - if (firstPatternFound) { - if(timeout !== undefined) { - clearTimeout(timeout); - console.log("MonerodProcess.start(): Cleared timeout"); - } - else { - console.log("MonerodProcess.start(): No timeout found"); - } - - resolve(); - } - else { - firstPatternFound = true; - } - }; - - if (waitForPattern) this.onStdOut(onStdOut); - else resolve(); - }); - - await super.start(); - - if (!this._process || !this._process.pid) { - throw new Error("Monerod process did not start!"); + console.log(message); + + let firstPatternFound = false; + const waitForPattern = this._args ? !this.privnet && !this._args.includes('--version') && !this.args.includes('--help') : true; + + const patternPromise = new Promise((resolve, reject) => { + let firstStdout = true; + let timeout: NodeJS.Timeout | undefined = undefined; + + const onStdOut = (out: string) => { + //console.log(out); + if (firstStdout) { + firstStdout = false; + + if (!waitForPattern) { + return; + } + + timeout = setTimeout(() => { + if (this._process && this._process.exitCode == null) { + this._process.kill(); + } + timeout = undefined; + + reject(new Error("MonerodProcess.start(): Timed out")); + }, 90*1000); } - try { - console.log(`MonerodProcess.start(): wait for pattern: ${waitForPattern}`); - if (waitForPattern) await patternPromise; + const foundPattern = out.includes(MonerodProcess.stdoutPattern); - console.log("Started monerod process pid: " + this._process.pid); + if (!foundPattern) { + return; } - catch(error: any) { - this._running = false; - this._starting = false; - this._stopping = false; - - if (error instanceof Error) { - throw error; - } - else { - throw new Error(`${error}`); - } + + if (firstPatternFound) { + if(timeout !== undefined) { + clearTimeout(timeout); + console.log("MonerodProcess.start(): Cleared timeout"); + } + else { + console.log("MonerodProcess.start(): No timeout found"); + } + + resolve(); + } + else { + firstPatternFound = true; } + }; + if (waitForPattern) this.onStdOut(onStdOut); + else resolve(); + }); + + await super.start(); + + if (!this._process || !this._process.pid) { + throw new Error("Monerod process did not start!"); } + try { + console.log(`MonerodProcess.start(): wait for pattern: ${waitForPattern}`); + + if (waitForPattern) await patternPromise; - public async getVersion(): Promise { - const proc = new MonerodProcess({ - monerodCmd: this._command, - flags: [ '--version' ], - isExe: this._isExe - }); - - const promise = new Promise((resolve, reject) => { - proc.onError((err: Error) => { - console.log("MonerodProcess.getVersion(): proc.onError():"); - console.error(err); - reject(err) - }); - - proc.onStdErr((err: string) => { - console.log("MonerodProcess.getVersion(): proc.onStdErr()"); - console.error(err); - reject(new Error(err)); - }); - - proc.onStdOut((version: string) => { - if (version == '') { - return; - } - - console.log("MonerodProcess.getVersion(): proc.onStdOut():"); - console.log(version); - resolve(version); - }); - }); - - console.log("MonerodProcess.getVersion(): Before proc.start()"); - await proc.start(); - console.log("MonerodProcess.getVersion(): After proc.start()"); - - return await promise; + console.log("Started monerod process pid: " + this._process.pid); } + catch(error: any) { + this._running = false; + this._starting = false; + this._stopping = false; + + if (error instanceof Error) { + throw error; + } + else { + throw new Error(`${error}`); + } + } + + } + + public async getVersion(): Promise { + const proc = new MonerodProcess({ + monerodCmd: this._command, + flags: [ '--version' ], + isExe: this._isExe + }); + + const promise = new Promise((resolve, reject) => { + proc.onError((err: Error) => { + console.log("MonerodProcess.getVersion(): proc.onError():"); + console.error(err); + reject(err) + }); + + proc.onStdErr((err: string) => { + console.log("MonerodProcess.getVersion(): proc.onStdErr()"); + console.error(err); + reject(new Error(err)); + }); + + proc.onStdOut((version: string) => { + if (version == '') { + return; + } + + console.log("MonerodProcess.getVersion(): proc.onStdOut():"); + console.log(version); + resolve(version); + }); + }); + + console.log("MonerodProcess.getVersion(): Before proc.start()"); + await proc.start(); + console.log("MonerodProcess.getVersion(): After proc.start()"); + + return await promise; + } } \ No newline at end of file diff --git a/app/process/index.ts b/app/process/index.ts index 584b507..fde3fe7 100644 --- a/app/process/index.ts +++ b/app/process/index.ts @@ -3,4 +3,4 @@ export { ProcessStats } from "./ProcessStats"; export { AppChildProcess } from "./AppChildProcess"; export { MonerodProcess } from "./MonerodProcess"; export { PrivateTestnet } from "./PrivateTestnet"; -export { I2pdProcess } from "./I2pdProcess"; \ No newline at end of file +export * from "./I2pdProcess"; \ No newline at end of file diff --git a/app/utils/BatteryUtils.ts b/app/utils/BatteryUtils.ts index 9f94493..31d3f22 100644 --- a/app/utils/BatteryUtils.ts +++ b/app/utils/BatteryUtils.ts @@ -5,35 +5,37 @@ import { powerMonitor } from "electron"; const batteryLevel = require('battery-level'); export abstract class BatteryUtils { - - public static async isOnBatteryPower(): Promise { - const onBattery = powerMonitor.isOnBatteryPower(); - - if (!onBattery && os.platform() == 'linux') { - return await new Promise((resolve) => { - exec("upower -i $(upower -e | grep 'battery') | grep 'state'", (error: ExecException | null, stdout: string) => { - if (error) { - console.error(`isOnBatteryPower(): ${error.message}`); - resolve(false); - return; - } - - const isOnBattery = stdout.includes("discharging"); - resolve(isOnBattery); - }); - }); - } - - return onBattery; + + public static async isOnBatteryPower(): Promise { + + const onBattery = powerMonitor.isOnBatteryPower(); + + if (!onBattery && os.platform() == 'linux') { + + return await new Promise((resolve) => { + exec("upower -i $(upower -e | grep 'battery') | grep 'state'", (error: ExecException | null, stdout: string) => { + if (error) { + console.error(`isOnBatteryPower(): ${error.message}`); + resolve(false); + return; + } + + const isOnBattery = stdout.includes("discharging"); + resolve(isOnBattery); + }); + }); } - public static async getLevel(): Promise { - try { - return batteryLevel(); - } - catch(error: any) { - console.error(error); - return -1; - } + return onBattery; + } + + public static async getLevel(): Promise { + try { + return batteryLevel(); + } + catch(error: any) { + console.error(error); + return -1; } + } } \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cc0bba0..605f167 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -28,6 +28,7 @@ import { HardForkInfoModule } from './pages/hard-fork-info/hard-fork-info.module import { NetworkModule } from './pages/network/network.module'; import { PeersModule } from './pages/peers/peers.module'; import { AboutModule } from './pages/about/about.module'; +import { I2pWebconsoleModule } from './pages/i2p-webconsole/i2p-webconsole.module'; // AoT requires an exported function for factories const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -51,6 +52,7 @@ const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new Transl VersionModule, NetworkModule, AboutModule, + I2pWebconsoleModule, TranslateModule, AppRoutingModule, TranslateModule.forRoot({ diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index 785c09c..61dcaad 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -29,6 +29,7 @@ import { ElectronService } from '../electron/electron.service'; import { openDB, IDBPDatabase } from "idb" import { AxiosHeaders, AxiosResponse } from 'axios'; import { StringUtils } from '../../utils'; +import { I2pDaemonService } from '../i2p/i2p-daemon.service'; @Injectable({ providedIn: 'root' @@ -86,14 +87,11 @@ export class DaemonService { "Access-Control-Allow-Methods": 'POST,GET' // this states the allowed methods }; - constructor(private installer: MoneroInstallerService, private electronService: ElectronService) { + constructor(private installer: MoneroInstallerService, private electronService: ElectronService, private i2pService: I2pDaemonService) { this.openDbPromise = this.openDatabase(); this.settings = new DaemonSettings(); window.electronAPI.onMoneroClose((event: any, code: number) => { - console.debug(event); - console.debug(code); - if (code != 0) { window.electronAPI.showNotification({ title: 'Daemon Error', @@ -251,7 +249,6 @@ export class DaemonService { return await checkPromise; } - public async checkValidI2pdPath(path: string): Promise { if (path == null || path == undefined || path.replace(' ', '') == '') { return false; @@ -264,7 +261,6 @@ export class DaemonService { return await checkPromise; } - public async getSettings(): Promise { const db = await this.openDbPromise; const result = await db.get(this.storeName, 1); @@ -459,6 +455,12 @@ export class DaemonService { this.settings.noSync = true; } + if (this.i2pService.settings.enabled && !this.i2pService.running) { + console.log('starting i2pd service'); + await this.i2pService.start(); + console.log('started i2pd service'); + } + const startPromise = new Promise((resolve, reject) => { window.electronAPI.onMonerodStarted((event: any, started: boolean) => { console.debug(event); @@ -1077,6 +1079,13 @@ export class DaemonService { if (!this.restarting) { if (!this.quitting) { + + if (this.i2pService.running) { + console.log('stopping i2pd service'); + await this.i2pService.stop(); + console.log('stopped i2pd service'); + } + window.electronAPI.showNotification({ title: 'Daemon stopped', body: 'Successfully stopped monero daemon' @@ -1307,6 +1316,12 @@ export class DaemonService { return await getProcessStatsPromise; } + public async detectInstallation(): Promise<{ path: string; } | undefined> { + return await new Promise<{ path: string; } | undefined>((resolve) => { + window.electronAPI.detectInstallation('monerod', resolve); + }); + } + private _quitting: boolean = false; public get quitting(): boolean { diff --git a/src/app/core/services/electron/electron.service.ts b/src/app/core/services/electron/electron.service.ts index a19582e..609f383 100644 --- a/src/app/core/services/electron/electron.service.ts +++ b/src/app/core/services/electron/electron.service.ts @@ -1,4 +1,4 @@ -import { EventEmitter, Injectable } from '@angular/core'; +import { EventEmitter, Injectable, NgZone } from '@angular/core'; // If you import a module but never use any of the imported values other than as TypeScript types, // the resulting javascript file will look as if you never imported the module at all. import * as childProcess from 'child_process'; @@ -24,7 +24,7 @@ export class ElectronService { return this._isProduction; } - constructor() { + constructor(private ngZone: NgZone) { this._online = navigator.onLine; window.addEventListener('online', () => this._online = true); window.addEventListener('offline', () => this._online = false); @@ -77,7 +77,9 @@ export class ElectronService { public async isOnBatteryPower(): Promise { const promise = new Promise((resolve) => { window.electronAPI.isOnBatteryPower((onBattery: boolean) => { - resolve(onBattery); + this.ngZone.run(() => { + resolve(onBattery); + }); }); }); @@ -87,7 +89,6 @@ export class ElectronService { public async getBatteryLevel(): Promise { const promise = new Promise((resolve) => { window.electronAPI.getBatteryLevel((level: number) => { - window.electronAPI.unregisterOnGetBatteryLevel(); resolve(level); }); }); diff --git a/src/app/core/services/i2p/i2p-daemon.service.ts b/src/app/core/services/i2p/i2p-daemon.service.ts index 36b183d..e0beb71 100644 --- a/src/app/core/services/i2p/i2p-daemon.service.ts +++ b/src/app/core/services/i2p/i2p-daemon.service.ts @@ -10,6 +10,9 @@ export class I2pDaemonService { private readonly storeName = 'settingsStore'; private readonly openDbPromise: Promise; + public readonly onStart: EventEmitter = new EventEmitter(); + public readonly onStop: EventEmitter = new EventEmitter(); + private _settings: I2pDaemonSettings = new I2pDaemonSettings(); private _running: boolean = false; private _starting: boolean = false; @@ -48,9 +51,12 @@ export class I2pDaemonService { return this._loaded; } + private detectedInstallation?: { path: string; configFile?: string; tunnelConfig?: string; tunnelsConfigDir?: string; pidFile?: string; isRunning?: boolean; }; + constructor() { this.openDbPromise = this.openDatabase(); + this.loadSettings(); } private async openDatabase(): Promise> { @@ -75,16 +81,19 @@ export class I2pDaemonService { if (this.starting) throw new Error("Alrady starting i2pd"); this._starting = true; const promise = new Promise((resolve, reject) => { - window.electronAPI.onI2pdOutput((stdout?: string, stderr?: string) => { + + window.electronAPI.onI2pdOutput(({stdout, stderr} : { stdout?: string, stderr?: string }) => { if (stdout) { this._logs.push(stdout); this.std.out.emit(stdout); } else if (stderr) { this._logs.push(stderr); this.std.err.emit(stderr); + this._running = false; } }); - window.electronAPI.startI2pd(_config.path, _config.toFlags(), (error?: any) => { + + window.electronAPI.startI2pd(_config.path, (error?: any) => { this._starting = false; if (error) reject(new Error(`${error}`)); else resolve(); @@ -94,6 +103,7 @@ export class I2pDaemonService { await promise; this.setSettings(_config); this._running = true; + this.onStart.emit(); } public async stop(): Promise { @@ -113,6 +123,7 @@ export class I2pDaemonService { await promise; this._running = false; + if (!this.restarting) this.onStop.emit(); } public async restart(): Promise { @@ -152,11 +163,19 @@ export class I2pDaemonService { const result = await db.get(this.storeName, 1); if (result) { - this._settings = I2pDaemonSettings.parse(result); + this.setSettings(I2pDaemonSettings.parse(result)); } else { - this._settings = new I2pDaemonSettings(); + this.setSettings(new I2pDaemonSettings()); + } + + if (!this.detectedInstallation) { + this.detectedInstallation = await this.detectInstallation(); + } + + if (this._settings.path === '' && this.detectedInstallation) { + this._settings.path = this.detectedInstallation.path; } this._loaded = true; @@ -188,6 +207,12 @@ export class I2pDaemonService { public clearLogs(): void { this._logs = []; } + + private async detectInstallation(): Promise<{ path: string; configFile?: string; tunnelConfig?: string; tunnelsConfigDir?: string; pidFile?: string; isRunning?: boolean; } | undefined> { + return await new Promise<{ path: string; } | undefined>((resolve) => { + window.electronAPI.detectInstallation('i2pd', resolve); + }); + } } export interface I2pStd { diff --git a/src/app/pages/base-page/base-page.component.ts b/src/app/pages/base-page/base-page.component.ts index 080ea30..c1eb6b7 100644 --- a/src/app/pages/base-page/base-page.component.ts +++ b/src/app/pages/base-page/base-page.component.ts @@ -18,17 +18,20 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { protected subscriptions: Subscription[] = []; private readonly mResizeHandler: (event: Event) => void = (event: Event) => setTimeout(() => { - console.debug(event); - this.updateTableContentHeight(); + this.updateTablesContentHeight(); }, 100); constructor(private navbarService: NavbarService) { this.subscriptions.push(this.navbarService.onDaemonStatusChanged.subscribe((running) => { - if (running) setTimeout(() => this.updateTableContentHeight(), 100); + if (running) setTimeout(() => this.updateTablesContentHeight(), 100); })); window.addEventListener('resize', this.mResizeHandler); - } + } + + public updateTablesContentHeight(): void { + + } protected setLinks(links: NavbarLink[] = []): void { this._links = links; @@ -181,8 +184,6 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { public ngAfterContentInit(): void { this.navbarService.setLinks(this._links); - - setTimeout(() => this.updateTableContentHeight(), 100); } public ngOnDestroy(): void { @@ -196,14 +197,9 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { this.destroyTables(); } - public getTableContent(): HTMLElement | undefined { - const elements = document.getElementsByClassName('tab-content tab-grow'); + public getTableContent(id: string): HTMLElement | undefined { - if (!elements || elements.length === 0) { - return undefined; - } - - const element = elements[0]; + const element = document.getElementById(id); if (!(element instanceof HTMLElement)) { return undefined; @@ -212,7 +208,7 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { return element; } - public updateTableContentHeight(): void { + public updateTableContentHeight(id: string): void { if (!visualViewport) { return; } @@ -223,13 +219,10 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { return; } - //console.log(`view height: ${viewHeight}`); - const offset = 35; - const tab = this.getTableContent(); + const tab = this.getTableContent(id); if (!tab) { - //console.warn("table content not found"); return; } @@ -254,9 +247,9 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { tab.style.height = `${newHeight}px`; } - public scrollTableContentToBottom(): void { + public scrollTableContentToBottom(id: string): void { setTimeout(() => { - const tabContent = this.getTableContent(); + const tabContent = this.getTableContent(id); if (!tabContent) { console.warn("Could not find logs tab"); diff --git a/src/app/pages/detail/detail.component.ts b/src/app/pages/detail/detail.component.ts index de115df..5f25f12 100644 --- a/src/app/pages/detail/detail.component.ts +++ b/src/app/pages/detail/detail.component.ts @@ -1,7 +1,7 @@ import { Component, AfterViewInit, NgZone } from '@angular/core'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; import { NavbarService } from '../../shared/components/navbar/navbar.service'; -import { DaemonService, DaemonDataService } from '../../core/services'; +import { DaemonService, DaemonDataService, I2pDaemonService } from '../../core/services'; import { Subscription } from 'rxjs'; import { Connection, Span, Peer } from '../../../common'; import { SimpleBootstrapCard } from '../../shared/utils'; @@ -134,6 +134,14 @@ export class DetailComponent extends BasePageComponent implements AfterViewInit return this.daemonData.info ? this.daemonData.info.offline ? 'offline' : 'online' : 'offline'; } + private get connectionType(): 'Clearnet' | 'I2P' | 'TOR' { + if (this.i2pService.settings.enabled) { + return 'I2P'; + } + + return 'Clearnet'; + } + private get txCount(): number { return this.daemonData.info ? this.daemonData.info.txCount : 0; } @@ -197,7 +205,8 @@ export class DetailComponent extends BasePageComponent implements AfterViewInit public cards: SimpleBootstrapCard[]; constructor( - private daemonService: DaemonService, + private daemonService: DaemonService, + private i2pService: I2pDaemonService, navbarService: NavbarService, private daemonData: DaemonDataService, private ngZone: NgZone) { @@ -273,6 +282,7 @@ export class DetailComponent extends BasePageComponent implements AfterViewInit cards.push( new SimpleBootstrapCard('Connection Status', this.connectionStatus, loading), + new SimpleBootstrapCard('Connection Type', this.connectionType, loading), new SimpleBootstrapCard('Network Type', this.networkType, loading), new SimpleBootstrapCard('Node Type', this.nodeType, loading), new SimpleBootstrapCard('Sync progress', this.syncProgress, loading), diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole-routing.module.ts b/src/app/pages/i2p-webconsole/i2p-webconsole-routing.module.ts new file mode 100644 index 0000000..a79f486 --- /dev/null +++ b/src/app/pages/i2p-webconsole/i2p-webconsole-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { I2pWebconsoleComponent } from './i2p-webconsole.component'; + +const routes: Routes = [ + { + path: 'i2pwebconsole', + component: I2pWebconsoleComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class I2pWebconsoleRoutingModule { } diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole.component.html b/src/app/pages/i2p-webconsole/i2p-webconsole.component.html new file mode 100644 index 0000000..2d950f8 --- /dev/null +++ b/src/app/pages/i2p-webconsole/i2p-webconsole.component.html @@ -0,0 +1,168 @@ + +
+

I2P

+
+ +
+
+
+ +
+
+
+
+
+
Info
+
    +
  • Uptime: {{ mainData.uptime }}
  • +
  • Network Status: {{ mainData.networkStatus }}
  • +
  • Client Tunnels: {{ mainData.clientTunnels }}
  • +
  • Transit Tunnels: {{ mainData.transitTunnels }}
  • +
  • Tunnel Creation Success Rate: {{ mainData.tunnelCreationSuccessRate }}
  • +
  • Data Path: {{ mainData.dataPath }}
  • +
  • Router Identity: {{ mainData.routerIdent }}
  • +
  • Router Caps: {{ mainData.routerCaps }}
  • +
  • Routers: {{ mainData.routers }}
  • +
  • Version: {{ mainData.version }}
  • +
+
+
+
Services
+
    +
  • HTTP Proxy: {{ mainData.services.httpProxy ? 'Enabled' : 'Disabled' }}
  • +
  • SOCKS Proxy: {{ mainData.services.socksProxy ? 'Enabled' : 'Disabled' }}
  • +
  • BOB: {{ mainData.services.bob ? 'Enabled' : 'Disabled' }}
  • +
  • SAME: {{ mainData.services.same ? 'Enabled' : 'Disabled' }}
  • +
  • I2CP: {{ mainData.services.i2cp ? 'Enabled' : 'Disabled' }}
  • +
  • I2PControl: {{ mainData.services.i2pcControl ? 'Enabled' : 'Disabled' }}
  • +
+
+
+
+
+
Network
+
    +
  • Sent: {{ mainData.sent }}
  • +
  • Received: {{ mainData.received }}
  • +
+
+
+
+
+
+ +
+
+
+ +

Commands

+ +
+ +
+ +
+ +
+ Check network connectivity +
+ +
+ +
+ +
+ Reload configuration and restart tunnels +
+ +
+ +
+ +
+ Stop connectivity to transit tunnels +
+ +
+ +
+

Miscellaneous

+ +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+
+
+ +
+
+
+
+
+
Local Destinations
+
    + @for(dest of localDestinations.destinations; track dest) { +
  • {{ dest }}
  • + } +
+
+
+
+
+
+ +
+
+
+
+
+
Client Tunnels
+
    + @for(client of i2pTunnels.clients; track client.name) { +
  • {{ client.name }}: {{ client.address }}
  • + } +
+
+
+
Server Tunnels
+
    + @for(server of i2pTunnels.servers; track server.name) { +
  • {{ server.name }}: {{ server.address }}
  • + } +
+
+
+
+
+
+ + +
\ No newline at end of file diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole.component.scss b/src/app/pages/i2p-webconsole/i2p-webconsole.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole.component.spec.ts b/src/app/pages/i2p-webconsole/i2p-webconsole.component.spec.ts new file mode 100644 index 0000000..e6dccab --- /dev/null +++ b/src/app/pages/i2p-webconsole/i2p-webconsole.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { I2pWebconsoleComponent } from './i2p-webconsole.component'; + +describe('I2pWebconsoleComponent', () => { + let component: I2pWebconsoleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [I2pWebconsoleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(I2pWebconsoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole.component.ts b/src/app/pages/i2p-webconsole/i2p-webconsole.component.ts new file mode 100644 index 0000000..660559c --- /dev/null +++ b/src/app/pages/i2p-webconsole/i2p-webconsole.component.ts @@ -0,0 +1,106 @@ +import { Component, OnDestroy } from '@angular/core'; +import { BasePageComponent } from '../base-page/base-page.component'; +import { LocalDestinationsData, MainData, TunnelsData } from '../../../common'; +import { NavbarLink, NavbarService } from '../../shared/components'; + +@Component({ + selector: 'app-i2p-webconsole', + imports: [], + templateUrl: './i2p-webconsole.component.html', + styleUrl: './i2p-webconsole.component.scss', + standalone: false +}) +export class I2pWebconsoleComponent extends BasePageComponent implements OnDestroy { + private readonly host: string = 'http://127.0.0.1:7070'; + + private refreshing: boolean = false; + private refreshInterval?: NodeJS.Timeout; + + public mainData: MainData = new MainData(); + public localDestinations: LocalDestinationsData = new LocalDestinationsData(); + public i2pTunnels: TunnelsData = new TunnelsData(); + + constructor(navbarService: NavbarService) { + super(navbarService); + + const links = [ + new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', false, 'Overview'), + new NavbarLink('pills-router-commands-tab', '#pills-router-commands', 'pills-router-commands', false, 'Commands'), + new NavbarLink('pills-local-destinations-tab', '#pills-local-destinations', 'pills-local-destinations', false, 'Local Destinations'), + new NavbarLink('pills-i2p-tunnels-tab', '#pills-i2p-tunnels', 'pills-i2p-tunnels', false, 'I2P Tunnels'), + ]; + + this.setLinks(links); + this.startLoop(); + } + + private readonly refreshHandler: () => void = () => this.refresh(); + + private async refresh(): Promise { + if (this.refreshing) return; + this.refreshing = true; + + try { + this.mainData = await this.getMainData(); + this.localDestinations = await this.getLocalDestinations(); + this.i2pTunnels = await this.getI2pTunnels(); + } + catch (error: any) { + console.error(error); + } + + this.refreshing = false; + } + + private startLoop() { + if (this.refreshInterval !== undefined) throw new Error("loop already started"); + this.refresh().then(() => { + this.refreshInterval = setInterval(this.refreshHandler, 5000); + }).catch((error: any) => console.error(error)); + } + + private async fetchContent(request: string = ''): Promise { + return await new Promise((resolve, reject) => { + fetch(`${this.host}/${request}`) + .then(response => response.text()) + .then(html => { + const _content = document.createElement('div'); + _content.innerHTML = html; + + for (let i = 0; i < _content.children.length; i++) { + const child = _content.children.item(i); + if (!child) continue; + + if (child.className === 'wrapper') { + resolve(child as HTMLDivElement); + return; + } + } + + reject(new Error('Wrapper not found')); + }) + .catch(error => reject(error)); + }); + } + + public async getMainData(): Promise { + return MainData.fromWrapper(await this.fetchContent()) + } + + public async getLocalDestinations(): Promise { + return LocalDestinationsData.fromWrapper(await this.fetchContent('?page=local_destinations')); + } + + public async getI2pTunnels(): Promise { + return TunnelsData.fromWrapper(await this.fetchContent('?page=i2p_tunnels')); + } + + public override ngOnDestroy(): void { + super.ngOnDestroy(); + + if (this.refreshInterval === undefined) return; + clearInterval(this.refreshInterval); + } + +} + diff --git a/src/app/pages/i2p-webconsole/i2p-webconsole.module.ts b/src/app/pages/i2p-webconsole/i2p-webconsole.module.ts new file mode 100644 index 0000000..37f7de9 --- /dev/null +++ b/src/app/pages/i2p-webconsole/i2p-webconsole.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { I2pWebconsoleRoutingModule } from './i2p-webconsole-routing.module'; +import { I2pWebconsoleComponent } from './i2p-webconsole.component'; +import { SharedModule } from '../../shared/shared.module'; + + +@NgModule({ + declarations: [I2pWebconsoleComponent], + imports: [ + CommonModule, + SharedModule, + I2pWebconsoleRoutingModule + ] +}) +export class I2pWebconsoleModule { } diff --git a/src/app/pages/logs/logs.component.html b/src/app/pages/logs/logs.component.html index 1434915..a38d350 100644 --- a/src/app/pages/logs/logs.component.html +++ b/src/app/pages/logs/logs.component.html @@ -19,7 +19,7 @@

No logs

-
+
{{ logs }}
@@ -766,14 +766,14 @@

Set the log hash rate display mode

-
-
+
+

No logs

Start I2P daemon to enable session logging

-
+
{{ i2pdLogs }}
diff --git a/src/app/pages/logs/logs.component.ts b/src/app/pages/logs/logs.component.ts index 056d9c0..051366f 100644 --- a/src/app/pages/logs/logs.component.ts +++ b/src/app/pages/logs/logs.component.ts @@ -2,7 +2,7 @@ import { AfterViewInit, Component, NgZone, OnDestroy } from '@angular/core'; import { LogsService } from './logs.service'; import { NavbarService } from '../../shared/components/navbar/navbar.service'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; -import { DaemonService, I2pDaemonService } from '../../core/services'; +import { DaemonService } from '../../core/services'; import { LogCategories } from '../../../common'; import { BasePageComponent } from '../base-page/base-page.component'; import { Subscription } from 'rxjs'; @@ -41,34 +41,28 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, O return this.logsService.categories; } - constructor(navbarService: NavbarService, private i2pdService: I2pDaemonService, private logsService: LogsService, private daemonService: DaemonService, private ngZone: NgZone) { + constructor(navbarService: NavbarService, private logsService: LogsService, private daemonService: DaemonService, private ngZone: NgZone) { super(navbarService); - const onLogSub: Subscription = this.logsService.onLog.subscribe((message: string) => { - console.debug(message); - this.onLog() + const onLogSub: Subscription = this.logsService.onLog.subscribe(({ type } : { message: string; type: 'monerod' | 'i2pd'; }) => { + this.onLog(type); }); const links = [ - new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', false, 'Overview'), + new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', false, 'monerod'), + new NavbarLink('pills-i2pd-tab', '#pills-i2pd', 'pills-i2pd', false, 'i2pd'), new NavbarLink('pills-set-log-level-tab', '#pills-set-log-level', 'pills-set-log-level', false, 'Set Log Level'), new NavbarLink('pills-set-log-categories-tab', '#pills-set-log-categories', 'pills-set-log-categories', false, 'Set Log Categories'), new NavbarLink('pills-set-log-hash-rate-tab', '#pills-set-log-hash-rate', 'pills-set-log-hash-rate', false, 'Set Log Hash Rate') ]; - if (i2pdService.running) { - const link = new NavbarLink('pills-i2pd-tab', '#pills-i2pd', 'pills-i2pd', false, 'I2P') - - links.push(link); - } - this.setLinks(links); this.subscriptions.push(onLogSub); } public get lines(): string[] { - return this.logsService.lines; + return this.logsService.logs.monerod; } public get logs(): string { @@ -76,17 +70,17 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, O } public get i2pdLines(): string [] { - return this.i2pdService.logs; + return this.logsService.logs.i2pd; } public get i2pdLogs(): string { return this.initing ? '' : this.i2pdLines.join("\n"); } - private onLog(): void { + private onLog(type: 'monerod' | 'i2pd'): void { if (this.scrolling) return; - this.scrollTableContentToBottom(); + this.scrollTableContentToBottom(`${type}-log-table`); } private registerScrollEvents(): void { @@ -95,33 +89,45 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, O return; } - const tab = this.getTableContent(); + const tabs = this.getTableContents(); - if (!tab) { - console.warn("Coult not find table content"); - return; - } + tabs.forEach((tab) => { + tab.addEventListener('scroll', this.scrollHandler); + tab.addEventListener('scrollend', this.scrollHandler); + }); - tab.addEventListener('scroll', this.scrollHandler); - tab.addEventListener('scrollend', this.scrollHandler); this.scrollEventsRegistered = true; } + private getTableContents(): HTMLElement[] { + const table1 = document.getElementById('monerod-log-table'); + const table2 = document.getElementById('i2pd-log-table'); + const result: HTMLElement[] = []; + + if (table1) result.push(table1); + if (table2) result.push(table2); + + return result; + } + private unregisterScrollEvents(): void { if (!this.scrollEventsRegistered) { console.warn("Scroll events already unregistered"); return; } - const tab = this.getTableContent(); + const tabs = this.getTableContents(); - if (!tab) { - console.warn("Coult not find table content"); - return; - } + tabs.forEach((tab) => { + if (!tab) { + console.warn("Coult not find table content"); + return; + } + + tab.removeEventListener('scroll', this.scrollHandler); + tab.removeEventListener('scrollend', this.scrollHandler); + }); - tab.removeEventListener('scroll', this.scrollHandler); - tab.removeEventListener('scrollend', this.scrollHandler); this.scrollEventsRegistered = false; } @@ -184,15 +190,31 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, O this.settingLogCategories = false; } + public override ngAfterContentInit(): void { + super.ngAfterContentInit(); + + setTimeout(() => this.updateTablesContentHeight(), 100); + } + + public override updateTablesContentHeight(): void { + this.updateTableContentHeight('monerod-log-table'); + this.updateTableContentHeight('i2pd-log-table'); + } + + public scrollTablesContentToBottom(): void { + this.scrollTableContentToBottom('monerod-log-table'); + this.scrollTableContentToBottom('i2pd-log-table'); + } + public ngAfterViewInit(): void { this.initing = true; setTimeout(() => { this.registerScrollEvents(); - this.scrollTableContentToBottom(); + this.scrollTablesContentToBottom(); this.initing = false; setTimeout(() => { - this.scrollTableContentToBottom(); + this.scrollTablesContentToBottom(); }, 500); }, 500); } diff --git a/src/app/pages/logs/logs.service.ts b/src/app/pages/logs/logs.service.ts index c992e07..fa7fbdb 100644 --- a/src/app/pages/logs/logs.service.ts +++ b/src/app/pages/logs/logs.service.ts @@ -1,35 +1,55 @@ import { EventEmitter, Injectable, NgZone } from '@angular/core'; import { LogCategories } from '../../../common'; +import { I2pDaemonService } from '../../core/services'; @Injectable({ providedIn: 'root' }) export class LogsService { - public readonly onLog: EventEmitter = new EventEmitter(); - public readonly lines: string[] = []; + private readonly i2pdLogHandler: (message: string) => void = (message: string) => { + this.log(message, 'i2pd'); + } + + private readonly monerodLogHandler: (event: any, message: string) => void = (event: any, message: string) => { + this.log(message, 'monerod'); + } + + public readonly onLog: EventEmitter<{ message: string, type: 'monerod' | 'i2pd' }> = new EventEmitter<{ message: string, type: 'monerod' | 'i2pd' }>(); + public readonly logs: { + monerod: string[]; + i2pd: string[]; + } = { + monerod: [], + i2pd: [] + } public readonly maxLines: number = 250; public readonly categories: LogCategories = new LogCategories(); - - constructor(private ngZone: NgZone) { - window.electronAPI.onMoneroStdout((event: any, message: string) => { - this.log(message); - }); + + constructor(private ngZone: NgZone, private i2pService: I2pDaemonService) { + window.electronAPI.onMoneroStdout(this.monerodLogHandler); + this.i2pService.std.out.subscribe(this.i2pdLogHandler); + this.i2pService.std.err.subscribe(this.i2pdLogHandler); } public cleanLog(message: string): string { return message.replace(/\u001b\[[0-9;]*m/g, '').replace(/[\r\n]+/g, '\n').trim(); // eslint-disable-line } - public log(message: string): void { + public log(message: string, type: 'monerod' | 'i2pd'): void { + const lines = type === 'monerod' ? this.logs.monerod : this.logs.i2pd; + this.ngZone.run(() => { - if (this.lines.length <= this.maxLines) { - this.lines.push(this.cleanLog(message)); + message = this.cleanLog(message); + + if (lines.length <= this.maxLines) { + lines.push(this.cleanLog(message)); } else { - this.lines.shift(); - this.lines.push(this.cleanLog(message)); + lines.shift(); + lines.push(this.cleanLog(message)); } - this.onLog.emit(this.cleanLog(message)); + + this.onLog.emit({ message, type }); }); } diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index c48bb52..58f1dc8 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -1098,16 +1098,16 @@

I2P Service

- + - +
Path to i2pd executable
- +
Enable I2P Service
@@ -1181,10 +1181,9 @@

I2P Anonymous Inbound

- + - Specify inbound hidden service port for receiving incoming I2P connections
diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 6095e02..4e6a4fe 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -21,7 +21,7 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni private addingExclusiveNode: boolean = false; private addingPriorityNode: boolean = false; - private originalSettings: DaemonSettings; + private originalSettings: DaemonSettings = new DaemonSettings(); private _currentSettings: DaemonSettings; private readonly _privnetSettings: DefaultPrivnetNode2Settings = new DefaultPrivnetNode2Settings(); @@ -59,8 +59,11 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni return this._currentSettings; } + public originalI2pdSettings: I2pDaemonSettings; + private _currentI2pdSettings: I2pDaemonSettings; + public get currentI2pdSettings(): I2pDaemonSettings { - return this.i2pdService.settings; + return this._currentI2pdSettings; } public get isPrivnet(): boolean { @@ -303,8 +306,9 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni new NavbarLink('pills-i2p-tab', '#pills-i2p', 'pills-i2p', false, 'I2P', false) ]; - this.originalSettings = new DaemonSettings(); this._currentSettings = this.originalSettings.clone(); + this.originalI2pdSettings = this.i2pdService.settings; + this._currentI2pdSettings = this.originalI2pdSettings.clone(); this.load().then(() => { console.debug("Settings loaded"); @@ -517,6 +521,16 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni } private async load(): Promise { + if (!this.i2pdService.loaded) + { + this.originalI2pdSettings = await this.i2pdService.loadSettings(); + } + else { + this.originalI2pdSettings = this.i2pdService.settings; + } + + this._currentI2pdSettings = this.originalI2pdSettings.clone(); + this.originalSettings = await this.daemonService.getSettings(); this._currentSettings = this.originalSettings.clone(); @@ -570,6 +584,9 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni if (!this._currentSettings.equals(this.originalSettings)) { return true; } + if (!this._currentI2pdSettings.equals(this.originalI2pdSettings)) { + return true; + } return false; } @@ -697,8 +714,11 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni const oldStartMinimized: boolean = this.originalSettings.startAtLoginMinimized; await this.daemonService.saveSettings(this._currentSettings); + this.i2pdService.setSettings(this._currentI2pdSettings); + await this.i2pdService.saveSettings(); this.originalSettings = this._currentSettings.clone(); + this.originalI2pdSettings = this._currentI2pdSettings.clone(); const minimizedChanged: boolean = oldStartMinimized != this.originalSettings.startAtLoginMinimized; @@ -822,7 +842,11 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni } public removeMonerodFile(): void { - this.currentSettings.monerodPath = ''; + this._currentSettings.monerodPath = ''; + } + + public removeI2pdFile(): void { + this._currentSettings.monerodPath = ''; } public async chooseMonerodFile(): Promise { @@ -857,9 +881,7 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni const valid = await this.daemonService.checkValidI2pdPath(file); if (valid) { this.ngZone.run(() => { - const currentSettings = this.currentSettings; - if (currentSettings instanceof PrivnetDaemonSettings) return; - else currentSettings.i2pdPath = file; + this.currentI2pdSettings.path = file; }); } else { diff --git a/src/app/shared/components/sidebar/sidebar.component.ts b/src/app/shared/components/sidebar/sidebar.component.ts index 8d82cda..fea8ccd 100644 --- a/src/app/shared/components/sidebar/sidebar.component.ts +++ b/src/app/shared/components/sidebar/sidebar.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { DaemonService } from '../../../core/services/daemon/daemon.service'; +import { I2pDaemonService } from '../../../core/services'; @Component({ selector: 'app-sidebar', @@ -23,15 +24,27 @@ export class SidebarComponent { return this.navLinks.filter((link) => link.position == 'bottom'); } - constructor(private router: Router, private daemonService: DaemonService) { + public get i2pEnabled(): boolean { + return this.i2pService.settings.enabled; + } + + constructor(private router: Router, private i2pService: I2pDaemonService) { this.updateLinks(); this.isLoading = false; this.errorMessage = ''; + + this.i2pService.onStart.subscribe(() => { + this.updateLinks(); + }); + + this.i2pService.onStop.subscribe(() => { + this.updateLinks(); + }); } private createFullLinks(): NavLink[] { // new NavLink('XMRig', '/xmrig', 'icon-xr text-primary'), - return this.navLinks = [ + this.navLinks = [ new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'), new NavLink('Blockchain', '/blockchain', 'bi bi-bounding-box'), new NavLink('Transactions', '/transactions', 'bi bi-credit-card-2-front'), @@ -41,16 +54,24 @@ export class SidebarComponent { new NavLink('Network', '/network', 'bi bi-hdd-network', 'bottom'), new NavLink('Peers', '/peers', 'bi bi-people', 'bottom'), new NavLink('Bans', '/bans', 'bi bi-ban', 'bottom'), - new NavLink('Logs', '/logs', 'bi bi-terminal', 'bottom'), + new NavLink('Logs', '/logs', 'bi bi-terminal', 'bottom') + ]; + + if (this.i2pEnabled) { + this.navLinks.push(new NavLink('I2P', '/i2pwebconsole', 'bi bi-incognito', 'bottom')); + } + + this.navLinks.push( new NavLink('Version', '/version', 'bi bi-git', 'bottom'), new NavLink('Settings', '/settings', 'bi bi-gear', 'bottom'), new NavLink('About', '/about', 'bi bi-info-circle', 'bottom') - ]; + ); + + return this.navLinks; } private updateLinks(): void { this.navLinks = this.createFullLinks(); - } public isActive(navLink: NavLink): boolean { diff --git a/src/common/Comparable.ts b/src/common/Comparable.ts new file mode 100644 index 0000000..5e48a05 --- /dev/null +++ b/src/common/Comparable.ts @@ -0,0 +1,43 @@ + + +export abstract class Comparable { + + constructor() { + } + + protected deepEqual(obj1: any, obj2: any): boolean { + // Se sono lo stesso riferimento, sono uguali + if (obj1 === obj2) return true; + + // Se uno dei due è nullo o non sono oggetti, non sono uguali + if (obj1 == null || obj2 == null || typeof obj1 !== 'object' || typeof obj2 !== 'object') { + return false; + } + + // Ottieni tutte le chiavi degli oggetti + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + // Se hanno un numero diverso di chiavi, non sono uguali + if (keys1.length !== keys2.length) return false; + + // Controlla che ogni chiave e valore sia equivalente + for (const key of keys1) { + // Se una chiave di obj1 non esiste in obj2, non sono uguali + if (!keys2.includes(key)) return false; + if (!this.deepEqual(obj1[key], obj2[key])) return false; + + // Se il valore della proprietà non è uguale, effettua un confronto ricorsivo + } + + return true; + } + + public abstract clone(): T; + + public equals(obj: T): boolean { + //return this.toCommandOptions().join('') == settings.toCommandOptions().join(''); + return this.deepEqual(this, obj); + } + +} \ No newline at end of file diff --git a/src/common/DaemonSettings.ts b/src/common/DaemonSettings.ts index 1b30811..7c7cb39 100644 --- a/src/common/DaemonSettings.ts +++ b/src/common/DaemonSettings.ts @@ -1,8 +1,8 @@ +import { Comparable } from "./Comparable"; import { DaemonSettingsDuplicateExclusiveNodeError, DaemonSettingsDuplicatePriorityNodeError, DaemonSettingsInvalidNetworkError, DaemonSettingsInvalidValueError, DaemonSettingsUnknownKeyError } from "./error"; -export class DaemonSettings { +export class DaemonSettings extends Comparable { public monerodPath: string = ''; - public i2pdPath: string = ''; public startAtLogin: boolean = false; public startAtLoginMinimized: boolean = false; @@ -236,12 +236,7 @@ export class DaemonSettings { } } - public equals(settings: DaemonSettings): boolean { - //return this.toCommandOptions().join('') == settings.toCommandOptions().join(''); - return this.deepEqual(this, settings); - } - - private deepEqual(obj1: any, obj2: any): boolean { + protected override deepEqual(obj1: any, obj2: any): boolean { // Se sono lo stesso riferimento, sono uguali if (obj1 === obj2) return true; @@ -286,7 +281,7 @@ export class DaemonSettings { return true; } - public clone(): DaemonSettings { + public override clone(): DaemonSettings { const result = Object.assign(new DaemonSettings(), this); result.exclusiveNodes = []; @@ -355,8 +350,6 @@ export class DaemonSettings { return this.isValidAnonymousInbound(inbound, 'i2p'); } - - public static isValidTxProxy(txProxy: string, type: 'tor' | 'i2p'): boolean { const netType = `${type},`; if (!txProxy.startsWith(netType)) { diff --git a/src/common/I2pDaemonSettings.ts b/src/common/I2pDaemonSettings.ts index 352d4a5..e0ecdce 100644 --- a/src/common/I2pDaemonSettings.ts +++ b/src/common/I2pDaemonSettings.ts @@ -1,101 +1,30 @@ -export interface I2pTunnelConfig { - type: string, - address: string, - port: number -}; +import { Comparable } from "./Comparable"; -export interface I2pTunnel extends I2pTunnelConfig { - name: string; -}; +export class I2pDaemonSettings extends Comparable { + public syncOnClearNet: boolean = true; + public allowIncomingConnections: boolean = true; + public txProxyEnabled: boolean = true; + public enabled: boolean = false; + public path: string = ""; -export interface I2pNetworkConfig { - enabled: boolean; - address: string; - port: number; -}; + public override clone(): I2pDaemonSettings { + const result = Object.assign(new I2pDaemonSettings(), this); -export class I2pDaemonSettings { - public syncOnClearNet: boolean = true; - public allowIncomingConnections: boolean = true; - public txProxyEnabled: boolean = true; - public enabled: boolean = false; - public path: string = ""; - public loglevel: string = "info"; - public ipv4: boolean = true; - public ipv6: boolean = false; - public floodfill: boolean = false; - public bandwidth: string = "X"; - public http: I2pNetworkConfig = { enabled: true, address: "127.0.0.1", port: 7070 }; - public socks: I2pNetworkConfig = { enabled: true, address: "127.0.0.1", port: 4447 }; - public i2cp: I2pNetworkConfig = { enabled: true, address: "127.0.0.1", port: 7654 }; - public tunnels: I2pTunnel[] = []; - public createHiddenService: boolean = true; // Nuova proprietà per gestire il hidden service - public hiddenServicePort: number = 80; + return result; + } - public static parse(settings: any): I2pDaemonSettings { - const result = new I2pDaemonSettings(); + public static parse(obj: any): I2pDaemonSettings { + const { syncOnClearNet, allowIncomingConnections, txProxyEnabled, enabled, path } = obj; - return result; - } + const result = new I2pDaemonSettings(); - constructor() { - } + if (typeof syncOnClearNet === 'boolean') result.syncOnClearNet = syncOnClearNet; + if (typeof allowIncomingConnections === 'boolean') result.allowIncomingConnections = allowIncomingConnections; + if (typeof txProxyEnabled === 'boolean') result.txProxyEnabled = txProxyEnabled; + if (typeof enabled === 'boolean') result.enabled = enabled; + if (typeof path === 'string') result.path = path; - public addTunnel(name: string, tunnelConfig: I2pTunnelConfig): void { - this.tunnels.push({ name, ...tunnelConfig }); - } + return result; + } - public toFlags(): string[] { - const flags: string[] = []; - - if (this.loglevel) { - flags.push(`--loglevel=${this.loglevel}`); - } - if (this.ipv4) { - flags.push('--ipv4'); - } - if (this.ipv6) { - flags.push('--ipv6'); - } - if (this.floodfill) { - flags.push('--floodfill'); - } - if (this.bandwidth) { - flags.push(`--bandwidth=${this.bandwidth}`); - } - - if (this.http.enabled) { - flags.push(`--http.address=${this.http.address}`); - flags.push(`--http.port=${this.http.port}`); - } - - if (this.socks.enabled) { - flags.push(`--socks.address=${this.socks.address}`); - flags.push(`--socks.port=${this.socks.port}`); - } - - if (this.i2cp.enabled) { - flags.push(`--i2cp.address=${this.i2cp.address}`); - flags.push(`--i2cp.port=${this.i2cp.port}`); - } - - if (this.createHiddenService) { - const hiddenServiceTunnel = this.tunnels.find(tunnel => tunnel.name === 'hidden_service'); - if (!hiddenServiceTunnel) { - this.addTunnel("hidden_service", { - type: "http", - address: "127.0.0.1", - port: this.hiddenServicePort - }); - } - } - - this.tunnels.forEach(tunnel => { - flags.push(`--tunnel.${tunnel.name}.type=${tunnel.type}`); - flags.push(`--tunnel.${tunnel.name}.address=${tunnel.address}`); - flags.push(`--tunnel.${tunnel.name}.port=${tunnel.port}`); - }); - - return flags; - } } \ No newline at end of file diff --git a/src/common/i2p/I2PData.ts b/src/common/i2p/I2PData.ts new file mode 100644 index 0000000..3ccb5bb --- /dev/null +++ b/src/common/i2p/I2PData.ts @@ -0,0 +1,3 @@ +export abstract class I2PData { + +} \ No newline at end of file diff --git a/src/common/i2p/LocalDestinationsData.ts b/src/common/i2p/LocalDestinationsData.ts new file mode 100644 index 0000000..12fbdf9 --- /dev/null +++ b/src/common/i2p/LocalDestinationsData.ts @@ -0,0 +1,21 @@ +import { I2PData } from "./I2PData"; + + +export class LocalDestinationsData extends I2PData { + public destinations: string[] = []; + + public static fromWrapper(wrapper: HTMLElement): LocalDestinationsData { + const items = wrapper.getElementsByClassName('listitem'); + const dests = new LocalDestinationsData(); + + for(let i = 0; i < items.length; i++) { + const item = items.item(i); + + if (!item || !item.textContent || item.textContent == '') continue; + + dests.destinations.push(item.textContent); + } + + return dests; + } +} \ No newline at end of file diff --git a/src/common/i2p/MainData.ts b/src/common/i2p/MainData.ts new file mode 100644 index 0000000..3b84706 --- /dev/null +++ b/src/common/i2p/MainData.ts @@ -0,0 +1,138 @@ + +export class MainData { + uptime: string = ''; + networkStatus: string = ''; + tunnelCreationSuccessRate: string = ''; + received: string = ''; + sent: string = ''; + transit: string = ''; + dataPath: string = ''; + routerIdent: string = ''; + routerCaps: string = ''; + version: string = ''; + ourExternalCaps: { type: string; address: string; }[] = []; + routers: string = ''; + clientTunnels: number = 0; + transitTunnels: number = 0; + services: { + httpProxy: boolean; + socksProxy: boolean; + bob: boolean; + same: boolean; + i2cp: boolean; + i2pControl: boolean; + } = { + httpProxy: false, + socksProxy: false, + bob: false, + same: false, + i2cp: false, + i2pControl: false + }; + + public static fromContent(content: string): MainData { + const components = content.split('\n').filter((c) => c !== ''); + const result = new MainData(); + + let constructingExternalAddresses: boolean = false; + let lastExternalAddress: { type: string; address: string; } | undefined = undefined; + + components.forEach((component) => { + if (component.includes('Uptime')) { + result.uptime = component.replace('Uptime: ', ''); + } + else if (component.includes('Network status')) { + result.networkStatus = component.replace('Network status: ', ''); + } + else if (component.includes('Tunnel creation success rate')) { + result.tunnelCreationSuccessRate = component.replace('Tunnel creation success rate: ', ''); + } + else if (component.includes('Received')) { + result.received = component.replace('Received: ', ''); + } + else if (component.includes('Sent')) { + result.sent = component.replace('Sent: ', ''); + } + else if (component.includes('Transit')) { + result.transit = component.replace('Transit: ', ''); + } + else if (component.includes('Data path')) { + result.dataPath = component.replace('Data path: ', ''); + } + else if (component.includes('Router Ident')) { + result.routerIdent = component.replace('Router Ident: ', ''); + } + else if (component.includes('Router Caps')) { + result.routerCaps = component.replace('Router Caps: ', ''); + } + else if (component.includes('Version')) { + result.version = component.replace('Version: ', ''); + } + else if (component.includes('Our external address')) { + constructingExternalAddresses = true; + return; + } + else if (component.includes('Routers')) { + result.routers = component.replace('Routers: ', ''); + } + else if (component.includes('Client Tunnels')) { + const v = component.replace('Client Tunnels: ', 'Transit Tunnels: ').split(' '); + const v0 = Number(v[0]); + const v1 = Number(v[1]); + + result.clientTunnels = (!isNaN(v0)) ? v0 : 0; + result.transitTunnels = (!isNaN(v1)) ? v1 : 0; + } + else if (component.includes('HTTP Proxy')) { + result.services.httpProxy = component.replace('HTTP Proxy', '') == 'Enabled'; + } + else if (component.includes('SOCKS Proxy')) { + result.services.socksProxy = component.replace('SOCKS Proxy', '') == 'Enabled'; + } + else if (component.includes('BOB')) { + result.services.bob = component.replace('BOB', '') == 'Enabled'; + } + else if (component.includes('I2CP')) { + result.services.i2cp = component.replace('I2CP', '') == 'Enabled'; + } + else if (component.includes('I2PControl')) { + result.services.bob = component.replace('I2PControl', '') == 'Enabled'; + } + else if (constructingExternalAddresses) { + if (lastExternalAddress) { + lastExternalAddress.address = component; + result.ourExternalCaps.push(lastExternalAddress); + lastExternalAddress = undefined; + } + else { + lastExternalAddress = { type: component, address: '' }; + } + + return; + } + + constructingExternalAddresses = false; + }); + + return result; + } + + public static fromWrapper(wrapper: HTMLDivElement): MainData { + for(let i = 0; i < wrapper.children.length; i++) { + const element = wrapper.children.item(i); + + if (!element) continue; + + if (element.className === 'content') { + return this.fromElement(element as HTMLElement); + } + } + + throw new Error("content not found"); + } + + public static fromElement(element: HTMLElement): MainData { + return (!element.textContent) ? new MainData() : this.fromContent(element.textContent); + } + +} \ No newline at end of file diff --git a/src/common/i2p/RouterCommandsData.ts b/src/common/i2p/RouterCommandsData.ts new file mode 100644 index 0000000..10e1599 --- /dev/null +++ b/src/common/i2p/RouterCommandsData.ts @@ -0,0 +1,4 @@ + +export class RouterCommandsData { + +} \ No newline at end of file diff --git a/src/common/i2p/TunnelsData.ts b/src/common/i2p/TunnelsData.ts new file mode 100644 index 0000000..6fb3652 --- /dev/null +++ b/src/common/i2p/TunnelsData.ts @@ -0,0 +1,50 @@ +import { I2PData } from "./I2PData"; + +interface TunnelInfo { + name: string; + address: string; +} + +export class TunnelsData extends I2PData { + public clients: TunnelInfo[] = []; + public servers: TunnelInfo[] = []; + + public static fromWrapper(wrapper: HTMLElement): TunnelsData { + const data = new TunnelsData(); + + const content = wrapper.getElementsByClassName('content').item(0); + + if (!content || !content.textContent || content.textContent == '') throw new Error("content not found"); + + const components = content.textContent.split('\n').filter((x) => x !== ''); + + const dc = ' ⇐ '; + const ds = ' ⇒ '; + let clients: boolean = false; + let servers: boolean = false; + + components.forEach((c) => { + if (c.startsWith('Client Tunnels:')) { + clients = true; + servers = false; + return; + } + else if (c.startsWith('Server Tunnels:')) { + servers = true; + clients = false; + return; + } + + const v = c.split(servers ? ds : dc); + + if (v.length == 2) { + const t: TunnelInfo = { name: v[0], address: v[1] }; + if (clients) data.clients.push(t); + else data.servers.push(t); + } + + }) + + return data; + } +} \ No newline at end of file diff --git a/src/common/i2p/index.ts b/src/common/i2p/index.ts new file mode 100644 index 0000000..8fcbbbc --- /dev/null +++ b/src/common/i2p/index.ts @@ -0,0 +1,5 @@ +export { I2PData } from './I2PData'; +export { MainData } from './MainData'; +export { RouterCommandsData } from './RouterCommandsData'; +export { LocalDestinationsData } from './LocalDestinationsData'; +export { TunnelsData } from './TunnelsData'; \ No newline at end of file diff --git a/src/common/index.ts b/src/common/index.ts index 6754b42..86421b7 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,3 +1,4 @@ +export { Comparable } from "./Comparable"; export { AddedAuxPow } from "./AddedAuxPow"; export { AuxPoW } from "./AuxPoW"; export { Ban } from "./Ban"; @@ -45,6 +46,7 @@ export { NetHashRateHistory, NetHashRateHistoryEntry } from './NetHashRateHistor export * from './error'; export * from './request'; export * from './utils'; +export * from './i2p'; export { PrivnetDaemonSettings } from "./PrivnetDaemonSettings"; export { DefaultPrivnetNode1Settings } from "./DefaultPrivnetNode1Settings"; diff --git a/src/polyfills.ts b/src/polyfills.ts index 416806f..00855ba 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -58,11 +58,12 @@ import { NotificationConstructorOptions } from 'electron'; declare global { interface Window { electronAPI: { + detectInstallation: (program: 'monerod' | 'i2pd' | 'tor' | 'monerod-gui', callback: (info?: any) => void) => void; httpPost: (params: { id: string; url: string; data?: any; config?: AxiosRequestConfig}, callback: (result: { data?: AxiosResponse, code: number; status: string; error?: string; }) => void) => void; httpGet: (params: { id: string; url: string; config?: AxiosRequestConfig }, callback: (result: { data?: AxiosResponse, code: number; status: string; error?: string; }) => void) => void; - startI2pd: (path: string, flags: string[], callback: (error?: any) => void) => void; + startI2pd: (path: string, callback: (error?: any) => void) => void; stopI2pd: (callback: (error?: any) => void) => void; - onI2pdOutput: (callback: (stdout?: string, stderr?: string) => void) => void; + onI2pdOutput: (callback: (output: {stdout?: string, stderr?: string}) => void) => void; copyToClipboard: (content: string) => void; startMonerod: (options: string[]) => void; stopMonerod: () => void; @@ -160,7 +161,6 @@ declare global { setTrayToolTip: (toolTip: string) => void; getBatteryLevel: (callback: (level: number) => void) => void; - unregisterOnGetBatteryLevel: () => void; isOnBatteryPower: (callback: (onBattery: boolean) => void) => void; onBattery: (callback: (event: any) => void) => void;