diff --git a/app/main.ts b/app/main.ts index 588e25d..b1504bf 100644 --- a/app/main.ts +++ b/app/main.ts @@ -574,8 +574,8 @@ try { win?.webContents.send(eventId, await detectInstallation(program)); }); - ipcMain.handle('start-i2pd', async (event: IpcMainInvokeEvent, params: { eventId: string; path: string; port: number; rpcPort: number; }) => { - const { eventId, path, port, rpcPort } = params; + ipcMain.handle('start-i2pd', async (event: IpcMainInvokeEvent, params: { eventId: string; path: string; port: number; rpcPort: number; outproxy?: { host: string; port: number; } }) => { + const { eventId, path, port, rpcPort, outproxy } = params; let error: string | undefined = undefined; @@ -588,10 +588,18 @@ try { else { try { //i2pdProcess = new I2pdProcess({ i2pdPath: path, flags, isExe: true }); - i2pdProcess = MoneroI2pdProcess.createSimple(path, port, rpcPort); + i2pdProcess = MoneroI2pdProcess.createSimple(path, port, rpcPort, outproxy); 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)); + i2pdProcess.onClose((_code: number | null) => { + const code = _code != null ? _code : -Number.MAX_SAFE_INTEGER; + const msg = `Process i2pd ${i2pdProcess?.pid} exited with code: ${code}`; + console.log(msg); + win?.webContents.send('i2pd-stdout', msg); + win?.webContents.send('i2pd-close', code); + monerodProcess = null; + }); } catch (err: any) { error = `${err}`; diff --git a/app/preload.js b/app/preload.js index e778c34..16c6cfd 100644 --- a/app/preload.js +++ b/app/preload.js @@ -26,14 +26,15 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.once(eventId, handler); ipcRenderer.invoke('check-valid-i2pd-path', { eventId, path }); }, - startI2pd: (path, port, rpcPort, callback) => { + startI2pd: (options, callback) => { + const { path, port, rpcPort, outproxy } = options; const eventId = `on-start-i2pd-${newId()}`; const handler = (event, result) => { callback(result); }; ipcRenderer.once(eventId, handler); - ipcRenderer.invoke('start-i2pd', { eventId, path, port, rpcPort }); + ipcRenderer.invoke('start-i2pd', { eventId, path, port, rpcPort, outproxy }); }, stopI2pd: (callback) => { const eventId = `on-stop-i2pd-${newId()}`; @@ -178,6 +179,11 @@ contextBridge.exposeInMainWorld('electronAPI', { const handler = (event, result) => callback(result); ipcRenderer.on('monero-close', handler); }, + onI2pdClose: (callback) => { + const handler = (event, result) => callback(result); + ipcRenderer.once('i2pd-close', handler); + }, + unregisterOnMoneroStdout: () => { ipcRenderer.removeAllListeners('monero-stdout'); }, diff --git a/app/process/I2pdProcess.ts b/app/process/I2pdProcess.ts index c1928c7..7ca11b5 100644 --- a/app/process/I2pdProcess.ts +++ b/app/process/I2pdProcess.ts @@ -263,7 +263,11 @@ export abstract class MoneroI2pTunnelConfigCreator { return 28089; } - public static createSimple(networkType: 'mainnet' | 'stagenet' | 'testnet' = 'mainnet', port?: number, rpcPort?: number): [I2pTunnelConfig, I2pTunnelConfig] { + 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 }) @@ -559,11 +563,15 @@ export class MoneroI2pdProcess extends I2pdProcess { ]; } - private static createDefaultConfigFile(): void { + private static createDefaultConfigFile(outproxy?: { host: string; port: number; }): void { if (!fs.existsSync(path.join(this.userDataPath, 'i2pd'))) { fs.mkdirSync(path.join(this.userDataPath, 'i2pd')); } + const enabled = outproxy !== undefined ? 'true' : 'false'; + const host = outproxy ? outproxy.host : '127.0.0.1'; + const port = outproxy ? outproxy. port : 9050; + fs.writeFileSync(this.defaultConfigPath, `ipv4 = true ipv6 = false daemon = false @@ -577,10 +585,9 @@ enabled = false [socksproxy] enabled = true -#outproxy.enabled = true -#outproxy = exit.stormycloud.i2p -#outproxy = 127.0.0.1 -#outproxyport = 9050 +outproxy.enabled = ${enabled} +outproxy = ${host} +outproxyport = ${port} [reseed] verify = true @@ -591,7 +598,7 @@ verify = true fs.writeFileSync(this.defaultTunnelsConfigPath, ``); } - private static createDefaultTunnelsDir(port: number, rpcPort: number): void { + private static createDefaultTunnelsDir(port: number, rpcPort: number, outproxy?: { host: string; port: number; }): void { const service = new MoneroI2pTunnelConfigService(path.join(this.defaultTunnelsConfigPath)); const result = MoneroI2pTunnelConfigCreator.createSimple('mainnet', port, rpcPort); service.setConfig(result[0], result[1], true); @@ -601,8 +608,8 @@ verify = true fs.writeFileSync(this.defaultLogPath, ``); } - public static createSimple(i2pdPath: string, port: number = 18080, rpcPort: number = 18081): MoneroI2pdProcess { - this.createDefaultConfigFile(); + public static createSimple(i2pdPath: string, port: number = 18080, rpcPort: number = 18081, outproxy?: { host: string; port: number; }): MoneroI2pdProcess { + this.createDefaultConfigFile(outproxy); if (!fs.existsSync(this.defaultTunnelsConfigPath)) { this.createDefaultTunnelsConfigFile(); diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index 9da488c..f23e1bd 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -74,6 +74,14 @@ export class DaemonService { public enablingSync: boolean = false; public startedAt?: Date; + public get startingI2pService(): boolean { + return this.i2pService.starting; + } + + public get stoppingI2pService(): boolean { + return this.i2pService.stopping; + } + public readonly onDaemonStatusChanged: EventEmitter = new EventEmitter(); public readonly onDaemonStopStart: EventEmitter = new EventEmitter(); public readonly onDaemonStopEnd: EventEmitter = new EventEmitter(); @@ -467,6 +475,11 @@ export class DaemonService { } catch (error: any) { console.error(error); + window.electronAPI.showNotification({ + title: 'Daemon error', + body: error instanceof Error ? error.message : `${error}`, + closeButtonText: 'Dismiss' + }); this.starting = false; throw error; } diff --git a/src/app/core/services/i2p/i2p-daemon.service.ts b/src/app/core/services/i2p/i2p-daemon.service.ts index 10ae248..1707305 100644 --- a/src/app/core/services/i2p/i2p-daemon.service.ts +++ b/src/app/core/services/i2p/i2p-daemon.service.ts @@ -18,6 +18,8 @@ export class I2pDaemonService { public readonly proxy: string = '127.0.0.1:4447'; public readonly txProxy: string = 'i2p,127.0.0.1:4447,disable_noise'; + public tunnelsData: TunnelsData = new TunnelsData(); + private _settings: I2pDaemonSettings = new I2pDaemonSettings(); private _running: boolean = false; private _starting: boolean = false; @@ -83,12 +85,38 @@ export class I2pDaemonService { }); } + private async waitForWebConsole(tries: number = 10): Promise { + for(let i = 0; i < tries; i++) { + try { + await this.getMainData(); + return; + } + catch { + continue; + } + } + + throw new Error("Failed connection to i2p webconsole"); + } + + private async checkI2PAlreadyRunningInSystem(): Promise { + try { + await this.waitForWebConsole(1); + return true; + } + catch { + return false; + } + } + public async start(config?: I2pDaemonSettings): Promise { const _config = config ? config : this._settings; if (this.running) throw new Error("Already running i2pd"); if (this.stopping) throw new Error("i2pd is stopping"); if (this.starting) throw new Error("Alrady starting i2pd"); + if (await this.checkI2PAlreadyRunningInSystem()) throw new Error("Another i2p service is already running"); + this._starting = true; const promise = new Promise((resolve, reject) => { @@ -103,7 +131,13 @@ export class I2pDaemonService { } }); - window.electronAPI.startI2pd(_config.path, _config.port, _config.rpcPort, (error?: any) => { + window.electronAPI.onI2pdClose((code: number) => { + console.log(code); + this._running = false; + this.onStop.emit(); + }); + + window.electronAPI.startI2pd(_config, (error?: any) => { this._starting = false; if (error) reject(new Error(`${error}`)); else resolve(); @@ -111,29 +145,86 @@ export class I2pDaemonService { }); await promise; + + this.tunnelsData = new TunnelsData(); + this._anonymousInbound = ''; + + try { + await this.waitForWebConsole(); + this.tunnelsData = await this.getI2pTunnels(); + this._anonymousInbound = await this.getAnonymousInbound(); + } + catch (error: any) { + console.error(error); + } + this.setSettings(_config); this._running = true; this.onStart.emit(); } + + private async shutdown(): Promise { + if (this.starting) throw new Error("i2pd is starting"); + if (!this.running) throw new Error("Already stopped i2pd"); + + await new Promise((resolve, reject) => { + window.electronAPI.onI2pdClose((code: number) => { + resolve(); + }); + + this.forceShutdown().then((result) => { + if (!result.message.includes('SUCCESS')) reject(new Error(result.message)); + }).catch((error: any) => reject(error instanceof Error ? error : new Error(`${error}`))); + }); + } + + private async stopGracefully(): Promise { + if (this.starting) throw new Error("i2pd is starting"); + if (!this.running) throw new Error("Already stopped i2pd"); + + await new Promise((resolve, reject) => { + window.electronAPI.onI2pdClose((code: number) => { + resolve(code); + }); + + this.startGracefulShutdown().then((result) => { + if (!result.message.includes('SUCCESS')) reject(new Error(result.message)); + }).catch((error: any) => reject(error instanceof Error ? error : new Error(`${error}`))); + }); + } - public async stop(): Promise { + public async stop(force: boolean = false): Promise { if (this.starting) throw new Error("i2pd is starting"); if (!this.running) throw new Error("Already stopped i2pd"); if (this.stopping) throw new Error("Alrady stopping i2pd"); this._stopping = true; - - const promise = new Promise((resolve, reject) => { - window.electronAPI.stopI2pd((error?: any) => { - this._stopping = false; - if (error) reject(new Error(`${error}`)); - else resolve(); - }); - }); + let err: any = null; - await promise; + try { + if (force) { + const promise = new Promise((resolve, reject) => { + window.electronAPI.stopI2pd((error?: any) => { + this._stopping = false; + if (error) reject(new Error(`${error}`)); + else resolve(); + }); + }); + + await promise; + } + else { + await this.stopGracefully(); + } + } + catch (error: any) { + err = error; + this._stopping = false; + } this._running = false; if (!this.restarting) this.onStop.emit(); + + if (err) throw err; } public async restart(): Promise { @@ -251,29 +342,8 @@ export class I2pDaemonService { return new Promise(resolveFunction); } - async function wait(d: number = 5000): Promise { - await new Promise((resolve) => setTimeout(resolve, d)); - } - - const tries: number = 3; - let err: any = null; - - for(let i = 0; i < tries; i++) { - try { - return await createPromise(); - } - catch (error: any) { - err = error; - - if (i != tries - 1) await wait(); - } - } - - if (err) { - throw err; - } + return await createPromise(); - throw new Error("Unknown error"); } public async getMainData(): Promise { diff --git a/src/app/pages/detail/detail.component.html b/src/app/pages/detail/detail.component.html index c908017..ee1c7d6 100644 --- a/src/app/pages/detail/detail.component.html +++ b/src/app/pages/detail/detail.component.html @@ -34,7 +34,19 @@

  
+
+ +
diff --git a/src/app/pages/detail/detail.component.ts b/src/app/pages/detail/detail.component.ts index 5f25f12..70f0db0 100644 --- a/src/app/pages/detail/detail.component.ts +++ b/src/app/pages/detail/detail.component.ts @@ -15,6 +15,48 @@ import { BasePageComponent } from '../base-page/base-page.component'; }) export class DetailComponent extends BasePageComponent implements AfterViewInit { + private parseAnonInboundUri(anonInbound: string): string { + const v = anonInbound.split(','); + const address = v[0]; + const socks = v[1]; + const c = socks.split(':'); + const port = c[1]; + return `${address}:${port}`; + } + + public get anonymousInbounds(): { uri: string; type: string; }[] { + const res: { uri: string; type: string; }[] = []; + + if (this.i2pService.running && this.i2pService.anonymousInbound.length > 0) { + const uri = this.parseAnonInboundUri(this.i2pService.anonymousInbound); + + res.push({ + uri, + type: 'P2P' + }); + } + + return res; + } + + public get anonymousServices(): { uri: string; type: string; }[] { + const res: { uri: string; type: string; }[] = []; + const servers = this.i2pService.tunnelsData.servers; + + if (this.i2pService.running && servers.length > 0) { + for (const server of servers) { + if (server.name === 'monero-rpc') { + res.push({ uri: `${server.address}`, type: 'RPC' }); + } + else if (server.name === 'monero-node') { + res.push({ uri: `${server.address}`, type: 'P2P' }); + } + } + } + + return res; + } + public get uptime(): { seconds: string, minutes: string, hours: string } { const startedAt = this.daemonService.startedAt; diff --git a/src/app/pages/logs/logs.component.html b/src/app/pages/logs/logs.component.html index a38d350..c3cc962 100644 --- a/src/app/pages/logs/logs.component.html +++ b/src/app/pages/logs/logs.component.html @@ -13,6 +13,8 @@

Logs

+ +

No logs

Start monero daemon to enable session logging

@@ -22,7 +24,7 @@

No logs

{{ logs }}
-
+
@@ -767,6 +769,8 @@

Set the log hash rate display mode

+ +

No logs

Start I2P daemon to enable session logging

@@ -776,7 +780,8 @@

No logs

{{ i2pdLogs }}
-
+
+
\ No newline at end of file diff --git a/src/app/pages/logs/logs.component.ts b/src/app/pages/logs/logs.component.ts index a6b6d77..a89b153 100644 --- a/src/app/pages/logs/logs.component.ts +++ b/src/app/pages/logs/logs.component.ts @@ -41,6 +41,9 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, A return this.logsService.categories; } + private readonly monerodLink: NavbarLink = new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', true, 'monerod', false, false); + private readonly i2pdLink: NavbarLink = new NavbarLink('pills-i2pd-tab', '#pills-i2pd', 'pills-i2pd', false, 'i2pd', false, false); + constructor(navbarService: NavbarService, private logsService: LogsService, private daemonService: DaemonService, private ngZone: NgZone) { super(navbarService); @@ -49,8 +52,7 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, A }); const links = [ - new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', false, 'monerod'), - new NavbarLink('pills-i2pd-tab', '#pills-i2pd', 'pills-i2pd', false, 'i2pd'), + this.monerodLink, this.i2pdLink, 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') @@ -136,6 +138,14 @@ export class LogsComponent extends BasePageComponent implements AfterViewInit, A return index; // usa l'indice per tracciare gli elementi } + public async clearMonerodLogs(): Promise { + this.logsService.clear('monerod'); + } + + public async clearI2pdLogs(): Promise { + this.logsService.clear('i2pd'); + } + public async setLogLevel(): Promise { this.settingLogLevel = true; diff --git a/src/app/pages/logs/logs.service.ts b/src/app/pages/logs/logs.service.ts index 371ae62..3ef86e2 100644 --- a/src/app/pages/logs/logs.service.ts +++ b/src/app/pages/logs/logs.service.ts @@ -35,6 +35,15 @@ export class LogsService { return message.replace(/\u001b\[[0-9;]*m/g, '').replace(/[\r\n]+/g, '\n').trim(); // eslint-disable-line } + public clear(program: 'monerod' | 'i2pd'): void { + if (program === 'monerod') { + this.logs.monerod = []; + } + else if (program === 'i2pd') { + this.logs.i2pd = []; + } + } + public log(message: string, type: 'monerod' | 'i2pd'): void { const lines = type === 'monerod' ? this.logs.monerod : this.logs.i2pd; diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index 757383e..8c63e36 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -1125,6 +1125,21 @@

I2P Service


Enable tx proxy through I2P service
+ +
+ + + + Outproxy ip + +
+ +
+ + + Outproxy port +
+

@@ -1141,7 +1156,7 @@

I2P Tx Proxy

- + Socks proxy port
diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 1c4fcb1..e3c5089 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -16,6 +16,9 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni public readonly navbarLinks: NavbarLink[]; + public i2pOutproxyIp: string = ''; + public i2pOutproxyPort: number = 0; + private removingExclusiveNodes: boolean = false; private removingPriorityNodes: boolean = false; @@ -199,7 +202,7 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni private get dbSyncMode(): string { return `${this.databaseSyncSpeed}:${this.databaseSyncMode}:${this.databaseSyncNBytesOrBlocks}${this.databaseSyncNPerMode}`; } - + // #region Validation public get validBanListUrl(): boolean { @@ -1147,6 +1150,14 @@ export class SettingsComponent extends BasePageComponent implements AfterViewIni else this._currentSettings.rpcLogin = `${this.rpcLoginUser}:${this.rpcLoginPassword}`; } + public onI2pOutproxyChange(): void { + if (this.i2pOutproxyIp != '' && this.i2pOutproxyPort > 0) { + this._currentI2pdSettings.outproxy = { host: this.i2pOutproxyIp, port: this.i2pOutproxyPort }; + } + else { + this._currentI2pdSettings.outproxy = undefined; + } + } } interface NodeInfo { address: string, port: number }; \ No newline at end of file diff --git a/src/app/shared/components/daemon-not-running/daemon-not-running.component.html b/src/app/shared/components/daemon-not-running/daemon-not-running.component.html index ae294c1..b61913d 100644 --- a/src/app/shared/components/daemon-not-running/daemon-not-running.component.html +++ b/src/app/shared/components/daemon-not-running/daemon-not-running.component.html @@ -13,7 +13,7 @@

D

Configure or install monero daemon

Daemon is starting

-

Starting monero daemon

+

{{ startingI2pService ? 'Starting i2p service' : 'Starting monero daemon' }}

Restarting monero daemon

Upgrading monero daemon to latest version

Installing monero daemon

@@ -24,7 +24,7 @@

Start