From 9063c27cd2d5be7bcd2fb2343da6c63d13a0caff Mon Sep 17 00:00:00 2001 From: argenius Date: Sat, 16 Nov 2024 01:23:24 +0100 Subject: [PATCH] Refactory, behavior consolidation and minor fixes --- .gitignore | 2 + app/main.ts | 681 ++++-------------- app/preload.js | 19 +- app/process/AppChildProcess.ts | 243 +++++++ app/process/AppMainProcess.ts | 90 +++ app/process/MonerodProcess.ts | 203 ++++++ app/process/ProcessStats.ts | 36 + app/process/index.ts | 4 + app/utils/BatteryUtils.ts | 39 + app/utils/FileUtils.ts | 185 +++++ app/utils/NetworkUtils.ts | 71 ++ app/utils/index.ts | 3 + .../core/services/daemon/daemon.service.ts | 12 + .../services/electron/electron.service.ts | 22 +- src/app/pages/bans/bans.component.html | 2 +- src/app/pages/logs/logs.component.html | 4 +- src/app/pages/mining/mining.component.html | 2 +- .../pages/settings/settings.component.html | 30 +- src/app/pages/settings/settings.component.ts | 36 +- .../transactions/transactions.component.html | 12 +- src/app/pages/version/version.component.html | 2 +- src/app/pages/version/version.component.ts | 52 +- src/common/DaemonSettings.ts | 18 + src/polyfills.ts | 7 +- 24 files changed, 1165 insertions(+), 610 deletions(-) create mode 100644 app/process/AppChildProcess.ts create mode 100644 app/process/AppMainProcess.ts create mode 100644 app/process/MonerodProcess.ts create mode 100644 app/process/ProcessStats.ts create mode 100644 app/process/index.ts create mode 100644 app/utils/BatteryUtils.ts create mode 100644 app/utils/FileUtils.ts create mode 100644 app/utils/NetworkUtils.ts create mode 100644 app/utils/index.ts diff --git a/.gitignore b/.gitignore index f31fe56..e2b0c27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ main.js src/**/*.js app/auto-launch/**/*.js +app/process/**/*.js +app/utils/**/*.js *.js.map # dependencies diff --git a/app/main.ts b/app/main.ts index f658483..4d3a084 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,95 +1,18 @@ import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions, - IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor + IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor, + WebContents, + HandlerDetails, + Event, + WebContentsWillNavigateEventParams } from 'electron'; -import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; -import * as https from 'https'; -import { createHash } from 'crypto'; -import * as tar from 'tar'; import * as os from 'os'; -import AutoLaunch from './auto-launch'; - -const AdmZip = require('adm-zip'); -const pidusage = require('pidusage'); -const batteryLevel = require('battery-level'); -const network = require('network'); - -function isOnBatteryPower(): Promise { - return new Promise((resolve) => { - exec("upower -i $(upower -e | grep 'battery') | grep 'state'", (error, stdout) => { - if (error) { - console.error(`isOnBatteryPower(): ${error.message}`); - resolve(false); // Ritorna false se non riesce a rilevare lo stato della batteria - return; - } - - const isOnBattery = stdout.includes("discharging"); - resolve(isOnBattery); - }); - }); -} - -interface Stats { - /** - * percentage (from 0 to 100*vcore) - */ - cpu: number; - - /** - * bytes - */ - memory: number; - - /** - * PPID - */ - ppid: number; - - /** - * PID - */ - pid: number; - - /** - * ms user + system time - */ - ctime: number; - - /** - * ms since the start of the process - */ - elapsed: number; - - /** - * ms since epoch - */ - timestamp: number; -} - -//import bz2 from 'unbzip2-stream'; -//import * as bz2 from 'unbzip2-stream'; -const bz2 = require('unbzip2-stream'); +import { AppMainProcess, MonerodProcess } from './process'; +import { BatteryUtils, FileUtils, NetworkUtils } from './utils'; app.setName('Monero Daemon'); -let autoLauncher = new AutoLaunch({ - name: 'monerod-gui', - path: process.execPath, - options: { - extraArguments: [ - '--auto-launch' - ], - linux: { - comment: 'Monerod GUI startup script', - version: '1.0.0' - } - } -}); - -const isAutoLaunched: boolean = process.argv.includes('--auto-launch'); -const minimized: boolean = process.argv.includes('--hidden'); - let win: BrowserWindow | null = null; let isHidden: boolean = false; let isQuitting: boolean = false; @@ -102,7 +25,9 @@ const dirname = (__dirname.endsWith(appApp) ? __dirname.replace(appApp, appSrc) console.log('dirname: ' + dirname); -let monerodProcess: ChildProcessWithoutNullStreams | null = null; +//let monerodProcess: ChildProcessWithoutNullStreams | null = null; +let monerodProcess: MonerodProcess | null = null; + const iconRelPath: string = 'assets/icons/monero-symbol-on-white-480.png'; //const wdwIcon = `${dirname}/${iconRelPath}`; const wdwIcon = path.join(dirname, iconRelPath); @@ -110,13 +35,6 @@ const wdwIcon = path.join(dirname, iconRelPath); let tray: Tray; let trayMenu: Menu; -const args = process.argv.slice(1), - serve = args.some(val => val === '--serve'); - -const isAppImage: () => boolean = () => { - return (!!process.env.APPIMAGE) || (!!process.env.PORTABLE_EXECUTABLE_DIR); -} - // #region Window function updateTrayMenu(): void { @@ -217,7 +135,7 @@ function createWindow(): BrowserWindow { webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, - allowRunningInsecureContent: (serve), + allowRunningInsecureContent: (AppMainProcess.serve), contextIsolation: true, devTools: !app.isPackaged, sandbox: true @@ -227,11 +145,11 @@ function createWindow(): BrowserWindow { icon: wdwIcon }); - isHidden = minimized; + isHidden = AppMainProcess.startMinized; if (!app.isPackaged) win.webContents.openDevTools(); - if (serve) { + if (AppMainProcess.serve) { const debug = require('electron-debug'); debug(); @@ -277,7 +195,7 @@ function createWindow(): BrowserWindow { const createSplashWindow = async (): Promise => { return undefined; - if (os.platform() == 'win32' || isAppImage()) { + if (os.platform() == 'win32' || AppMainProcess.isPortable) { return undefined; } @@ -320,129 +238,49 @@ const createSplashWindow = async (): Promise => { // #region WiFi -function isConnectedToWiFi(): Promise { +async function isWifiConnected() { + let connected: boolean = false; + try { - - return new Promise((resolve, reject) => { - network.get_active_interface((err: any | null, obj: { name: string, ip_address: string, mac_address: string, type: string, netmask: string, gateway_ip: string }) => { - if (err) { - console.error(err); - reject(err); - } - else { - resolve(obj.type == 'Wireless'); - } - }) - }); + connected = await NetworkUtils.isConnectedToWiFi(); } - catch(error: any) { - return isConnectedToWiFiV2(); - } -} - -function isConnectedToWiFiV2(): Promise { - return new Promise((resolve, reject) => { - const platform = os.platform(); // Use os to get the platform - - let command = ''; - if (platform === 'win32') { - // Windows: Use 'netsh' command to check the Wi-Fi status - command = 'netsh wlan show interfaces'; - } else if (platform === 'darwin') { - // macOS: Use 'airport' command to check the Wi-Fi status - command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep 'state: running'"; - } else if (platform === 'linux') { - // Linux: Use 'nmcli' to check for Wi-Fi connectivity - command = 'nmcli dev status'; - } else { - resolve(false); // Unsupported platform - } - - // Execute the platform-specific command - if (command) { - exec(command, (error: ExecException | null, stdout: string, stderr: string) => { - if (error) { - console.error(error); - reject(stderr); - resolve(false); // In case of error, assume not connected to Wi-Fi - } else { - // Check if the output indicates a connected status - if (stdout) { - const components: string[] = stdout.split("\n"); - - components.forEach((component: string) => { - if (component.includes('wifi') && !component.includes('--')) { - resolve(true); - } - }); - - resolve(false); - } else { - resolve(false); - } - } - }); - } - }); -} - -function isWifiConnected() { - isConnectedToWiFi().then((connected: boolean) => { - win?.webContents.send('is-wifi-connected-result', connected); - }).catch((error: any) => { + catch (error: any) { console.error(error); - win?.webContents.send('is-wifi-connected-result', false); - }); + connected = false; + } + + win?.webContents.send('is-wifi-connected-result', connected); } // #endregion // #region monerod -function getMonerodVersion(monerodFilePath: string): void { - const monerodProcess = spawn(monerodFilePath, [ '--version' ]); - - monerodProcess.on('error', (err: Error) => { - win?.webContents.send('monero-version-error', `${err.message}`); - }); - - monerodProcess.stdout.on('data', (data) => { - win?.webContents.send('monero-version', `${data}`); - }); - - monerodProcess.stderr.on('data', (data) => { - win?.webContents.send('monero-version-error', `${data}`); +async function getMonerodVersion(monerodFilePath: string): Promise { + const proc = new MonerodProcess({ + monerodCmd: monerodFilePath, + isExe: true }); + try { + console.log("Before proc.getVersion()"); + const version = await proc.getVersion(); + console.log("After proc.getVersion()"); + win?.webContents.send('monero-version', version); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; + win?.webContents.send('monero-version-error', err); + } } -function checkValidMonerodPath(monerodPath: string): void { - let foundUsage: boolean = false; - const monerodProcess = spawn(monerodPath, ['--help']); - - monerodProcess.on('error', (err: Error) => { - win?.webContents.send('on-check-valid-monerod-path', false); - }); - - monerodProcess.stderr.on('data', (data) => { - win?.webContents.send('on-check-valid-monerod-path', false); - }); - - monerodProcess.stdout.on('data', (data) => { - if (`${data}`.includes('monerod [options|settings] [daemon_command...]')) { - foundUsage = true; - } - }); - - monerodProcess.on('close', (code: number) => { - win?.webContents.send('on-check-valid-monerod-path', foundUsage); - }); - +async function checkValidMonerodPath(monerodPath: string): Promise { + const valid = await MonerodProcess.isValidMonerodPath(monerodPath); + + win?.webContents.send('on-check-valid-monerod-path', valid); } -let moneroFirstStdout: boolean = true; - -function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams { +async function startMoneroDaemon(commandOptions: string[]): Promise { const monerodPath = commandOptions.shift(); if (!monerodPath) { @@ -456,162 +294,78 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr win?.webContents.send('monero-stderr', error); throw new Error("Monerod already started"); } - - const message: string = "Starting monerod daemon with options: " + commandOptions.join(" "); - console.log(message); - - moneroFirstStdout = true; - commandOptions.push('--non-interactive'); - // Avvia il processo usando spawn - monerodProcess = spawn(monerodPath, commandOptions); - - // Gestisci l'output di stdout in streaming - monerodProcess.stdout.on('data', (data) => { - //console.log(`monerod stdout: ${data}`); - const pattern = '**********************************************************************'; - - if (moneroFirstStdout && data.includes(pattern)) { - win?.webContents.send('monerod-started', true); - moneroFirstStdout = false; - } - - win?.webContents.send('monero-stdout', `${data}`); - // Puoi anche inviare i log all'interfaccia utente tramite IPC + monerodProcess = new MonerodProcess({ + monerodCmd: monerodPath, + flags: commandOptions, + isExe: true }); - // Gestisci gli errori in stderr - monerodProcess.stderr.on('data', (data) => { - console.error(`monerod error: ${data}`); - - if (moneroFirstStdout) { - win?.webContents.send('monerod-started', false); - moneroFirstStdout = false; - } + monerodProcess.onStdOut((data) => { + win?.webContents.send('monero-stdout', `${data}`); + }); + monerodProcess.onStdErr((data) => { win?.webContents.send('monero-stderr', `${data}`); }); - // Gestisci la chiusura del processo - - monerodProcess.on('error', (err: Error) => { + monerodProcess.onError((err: Error) => { win?.webContents.send('monero-stderr', `${err.message}`); }); - monerodProcess.on('close', (code: number) => { + monerodProcess.onClose((_code: number | null) => { + const code = _code != null ? _code : -Number.MAX_SAFE_INTEGER; console.log(`monerod exited with code: ${code}`); win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`); win?.webContents.send('monero-close', code); monerodProcess = null; }); + try { + await monerodProcess.start(); + win?.webContents.send('monerod-started', true); + } + catch(error: any) { + win?.webContents.send('monerod-started', false); + } + return monerodProcess; } -function monitorMonerod(): void { +async function monitorMonerod(): Promise { if (!monerodProcess) { win?.webContents.send('on-monitor-monerod-error', 'Monerod not running'); return; } - if (!monerodProcess.pid) { - win?.webContents.send('on-monitor-monerod-error', 'Unknown monero pid'); - return; + try { + const stats = await monerodProcess.getStats(); + win?.webContents.send('on-monitor-monerod', stats); } + catch(error: any) { + let message: string; - pidusage(monerodProcess.pid, (error: Error | null, stats: Stats) => { - if (error) { - win?.webContents.send('on-monitor-monerod-error', `${error}`); - return; + if (error instanceof Error) { + message = error.message; + } + else { + message = `${error}`; } - win?.webContents.send('on-monitor-monerod', stats); - }); + win?.webContents.send('on-monitor-monerod-error', message); + } } // #endregion // #region Download Utils -const downloadFile = (url: string, destinationDir: string, onProgress: (progress: number) => void): Promise => { - return new Promise((resolve, reject) => { - const request = (url: string) => { - https.get(url, (response) => { - if (response.statusCode === 200) { - const contentDisposition = response.headers['content-disposition']; - let finalFilename = ''; - - // Estrai il nome del file dall'URL o dal content-disposition - if (contentDisposition && contentDisposition.includes('filename')) { - const match = contentDisposition.match(/filename="(.+)"/); - if (match) { - finalFilename = match[1]; - } - } else { - // Se non c'è content-disposition, prendiamo il nome dall'URL - finalFilename = url.split('/').pop() || 'downloaded-file'; - } - - const destination = `${destinationDir}/${finalFilename}`; - let file: fs.WriteStream; - - try { - file = fs.createWriteStream(destination); - file.on('error', (error: Error) => { - console.log("file error: " + error); - reject(error); - }); - } - catch (error: any) { - reject(error); - return; - } - - const totalBytes = parseInt(response.headers['content-length'] || '0', 10); - let downloadedBytes = 0; - - response.on('data', (chunk) => { - downloadedBytes += chunk.length; - const progress = (downloadedBytes / totalBytes) * 100; - onProgress(progress); // Notifica il progresso - }); - - response.pipe(file); - - file.on('finish', () => { - file.close(() => resolve(finalFilename)); // Restituisci il nome del file finale - }); - } else if (response.statusCode === 301 || response.statusCode === 302) { - // Se è un redirect, effettua una nuova richiesta verso il location header - const newUrl = response.headers.location; - if (newUrl) { - request(newUrl); // Ripeti la richiesta con il nuovo URL - } else { - reject(new Error('Redirection failed without a location header')); - } - } else { - reject(new Error(`Failed to download: ${response.statusCode}`)); - } - }).on('error', (err) => { - reject(err); - }); - }; - - request(url); // Inizia la richiesta - }); -}; - -// Funzione per scaricare e verificare l'hash -const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath: string): Promise => { - //const hashFilePath = path.join(app.getPath('temp'), 'monero_hashes.txt'); - - // Scarica il file di hash - const hashFileName = await downloadFile(hashUrl, app.getPath('temp'), () => {}); +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}`; - // Leggi il file di hash e cerca l'hash corrispondente const hashContent = fs.readFileSync(hashFilePath, 'utf8'); const hashLines = hashContent.split('\n'); let expectedHash: string | null = null; @@ -629,106 +383,10 @@ const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath } // Verifica l'hash del file scaricato - const calculatedHash = await verifyFileHash(`${filePath}/${fileName}`); - return calculatedHash === expectedHash; + return await FileUtils.checkFileHash(`${filePath}/${fileName}`, expectedHash); }; // Funzione per verificare l'hash del file -const verifyFileHash = (filePath: string): Promise => { - return new Promise((resolve, reject) => { - const hash = createHash('sha256'); - const fileStream = fs.createReadStream(filePath); - - fileStream.on('data', (data) => { - hash.update(data); - }); - - fileStream.on('end', () => { - resolve(hash.digest('hex')); - }); - - fileStream.on('error', (err) => { - reject(err); - }); - }); -}; - -const extractTarBz2 = (filePath: string, destination: string): Promise => { - return new Promise((resolve, reject) => { - // Crea il file decomprimendo il .bz2 in uno .tar temporaneo - const tarPath = path.join(destination, 'temp.tar'); - const fileStream = fs.createReadStream(filePath); - const decompressedStream = fileStream.pipe(bz2()); - - const writeStream = fs.createWriteStream(tarPath); - - decompressedStream.pipe(writeStream); - - let extractedDir: string = ''; - - writeStream.on('finish', () => { - // Una volta che il file .tar è stato creato, estrailo - tar.extract({ cwd: destination, file: tarPath, onReadEntry: (entry: tar.ReadEntry) => { - if (extractedDir == '') { - const topLevelDir = entry.path.split('/')[0]; - extractedDir = topLevelDir; // Salva la prima directory - } - } }) - .then(() => { - // Elimina il file .tar temporaneo dopo l'estrazione - fs.unlink(tarPath, (err) => { - if (err) reject(err); - else if (extractedDir == '') reject('Extraction failed') - else resolve(extractedDir); - }); - }) - .catch(reject); - }); - - writeStream.on('error', reject); - }); -}; - -const extractZip = (filePath: string, destination: string): Promise => { - return new Promise((resolve, reject) => { - try { - const zip = new AdmZip(filePath); - - // Ensure destination exists - if (!fs.existsSync(destination)) { - fs.mkdirSync(destination, { recursive: true }); - } - - // Extract the ZIP file - zip.extractAllTo(destination, true); - - // Get the name of the extracted folder - const extractedEntries = zip.getEntries(); - const folderName = extractedEntries[0]?.entryName.split('/')[0]; - - // Ensure folder name exists - if (!folderName) { - reject(new Error("Could not determine the extracted folder name")); - return; - } - - resolve(path.join(destination, folderName)); - } catch (error) { - reject(error); - } - }); -}; - -const extract = (filePath: string, destination: string): Promise => { - if (filePath.endsWith('.zip')) { - return extractZip(filePath, destination); - } - else if (filePath.endsWith('.tar.bz2')) { - return extractTarBz2(filePath, destination); - } - - throw new Error("Unknown file type " + filePath); -} // #endregion @@ -744,6 +402,21 @@ function showNotification(options?: NotificationConstructorOptions): void { new Notification(options).show(); } +function showSecurityWarning(msg: string): void { + if (win) { + dialog.showMessageBoxSync(win, { + type: 'warning', + title: 'Security Warning', + message: msg + }); + } + else { + dialog.showErrorBox('Security Warning', msg); + } + + console.warn(msg); +} + try { // This method will be called when Electron has finished // initialization and is ready to create browser windows. @@ -767,7 +440,7 @@ try { try { setTimeout(() => { if (splash) splash.close(); - if (!minimized) { + if (!AppMainProcess.startMinized) { win?.show(); win?.maximize(); } @@ -804,59 +477,43 @@ try { // #region Security - app.on('web-contents-created', (event, webContents) => { - webContents.setWindowOpenHandler((details) => { - console.warn("Prevented unsafe window creation"); + app.on('web-contents-created', (event, webContents: WebContents) => { + webContents.setWindowOpenHandler((details: HandlerDetails) => { + const msg = `Prevented unsafe content: ${details.url}`; + showSecurityWarning(msg); console.warn(details); + return { action: 'deny' }; }); + + webContents.on('will-navigate', (event: Event, navigationUrl: string) => { + event.preventDefault(); + const msg = `Prevented unsage window navigation to ${navigationUrl}`; + showSecurityWarning(msg); + }); }); - app.on('web-contents-created', (event, contents) => { - contents.on('will-navigate', (event, navigationUrl) => { - event.preventDefault(); - console.warn(`Prevented unsage window navigation to ${navigationUrl}`); - /* - const parsedUrl = new URL(navigationUrl) - - if (parsedUrl.origin !== 'https://example.com') { - event.preventDefault() - } - */ - }) - }) // #endregion - ipcMain.handle('is-on-battery-power', (event: IpcMainInvokeEvent) => { - const onBattery = powerMonitor.isOnBatteryPower(); - - if (!onBattery && os.platform() == 'linux') { - isOnBatteryPower().then((value) => { - win?.webContents.send('on-is-on-battery-power', value); - }).catch((error: any) => { - console.error(`${error}`); - win?.webContents.send('on-is-on-battery-power', false); - }); - - return; - } - else { - win?.webContents.send('on-is-on-battery-power', onBattery); - } - + ipcMain.handle('is-on-battery-power', async (event: IpcMainInvokeEvent) => { + const onBattery = await BatteryUtils.isOnBatteryPower(); + win?.webContents.send('on-is-on-battery-power', onBattery); }); powerMonitor.on('on-ac', () => win?.webContents.send('on-ac')); powerMonitor.on('on-battery', () => win?.webContents.send('on-battery')); - ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => { - console.debug(event); - - win?.webContents.send('on-is-auto-launched', isAutoLaunched); + ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-is-auto-launched', AppMainProcess.autoLaunched); }); - ipcMain.handle('quit', (event: IpcMainInvokeEvent) => { + ipcMain.handle('quit', async (event: IpcMainInvokeEvent) => { isQuitting = true; + + if (monerodProcess) { + await monerodProcess.stop(); + } + tray.destroy(); win?.close(); win?.destroy(); @@ -886,7 +543,7 @@ try { }); // Scarica il file Monero - const fileName = await downloadFile(downloadUrl, destination, (progress) => { + const fileName = await FileUtils.downloadFile(downloadUrl, destination, (progress) => { win?.setProgressBar(progress, { mode: 'normal' }); @@ -905,7 +562,7 @@ try { // Estrai il file const fPath = `${destination}/${fileName}`; event.sender.send('download-progress', { progress: 100, status: 'Extracting' }); - const extractedDir = await extract(fPath, destination); + const extractedDir = await FileUtils.extract(fPath, destination); event.sender.send('download-progress', { progress: 100, status: 'Download and extraction completed successfully' }); event.sender.send('download-progress', { progress: 200, status: os.platform() == 'win32' ? extractedDir : `${destination}/${extractedDir}` }); @@ -926,7 +583,7 @@ try { try { event.sender.send('download-file-progress', { progress: 0, status: 'Starting download' }); - const fileName = await downloadFile(url, destination, (progress) => { + const fileName = await FileUtils.downloadFile(url, destination, (progress) => { win?.setProgressBar(progress, { mode: 'normal' }); @@ -1045,87 +702,36 @@ try { // #region Auto Launch - ipcMain.handle('is-auto-launch-enabled', (event: IpcMainInvokeEvent) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - win?.webContents.send('on-is-auto-launch-enabled', enabled); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-is-auto-launch-enabled', false); - }); + ipcMain.handle('is-auto-launch-enabled', async (event: IpcMainInvokeEvent) => { + const enabled = await AppMainProcess.isAutoLaunchEnabled(); + win?.webContents.send('on-is-auto-launch-enabled', enabled); }); - ipcMain.handle('enable-auto-launch', (event: IpcMainInvokeEvent, minimized: boolean) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (enabled) { - win?.webContents.send('on-enable-auto-launch-error', 'already enabled'); - return; - } + ipcMain.handle('enable-auto-launch', async (event: IpcMainInvokeEvent, minimized: boolean) => { + try { + await AppMainProcess.enableAutoLaunch(minimized); + win?.webContents.send('on-enable-auto-launch-success'); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; - autoLauncher = new AutoLaunch({ - name: 'monerod-gui', - path: process.execPath, - options: { - launchInBackground: minimized, - extraArguments: [ - '--auto-launch' - ] - } - }); - - autoLauncher.enable().then(() => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (enabled) { - win?.webContents.send('on-enable-auto-launch-success'); - } - win?.webContents.send('on-enable-auto-launch-error', `Could not enabled auto launch`); - }).catch((error: any) => { - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); - - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-enable-auto-launch-error', `${error}`); - }); + win?.webContents.send('on-enable-auto-launch-error', err); + } }); - ipcMain.handle('get-battery-level', (event: IpcMainInvokeEvent) => { - batteryLevel().then((level: number) => { - win?.webContents.send('on-get-battery-level', level); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-get-battery-level', -1); - }) + ipcMain.handle('get-battery-level', async (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-get-battery-level', await BatteryUtils.getLevel()); }); - ipcMain.handle('disable-auto-launch', (event: IpcMainInvokeEvent) => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (!enabled) { - win?.webContents.send('on-disable-auto-launch-error', 'already disabled'); - return; - } - - autoLauncher.disable().then(() => { - autoLauncher.isEnabled().then((enabled: boolean) => { - if (!enabled) { - win?.webContents.send('on-disable-auto-launch-success'); - } - win?.webContents.send('on-disable-auto-launch-error', `Could not disable auto launch`); - }).catch((error: any) => { - win?.webContents.send('on-disable-auto-launch-error', `${error}`); - }); - - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-disable-auto-launch-error', `${error}`); - }); - }).catch((error: any) => { - console.error(error); - win?.webContents.send('on-disable-auto-launch-error', `${error}`); - }); + ipcMain.handle('disable-auto-launch', async (event: IpcMainInvokeEvent) => { + try { + await AppMainProcess.disableAutoLaunch(); + win?.webContents.send('on-disable-auto-launch-success'); + } + catch(error: any) { + const err = (error instanceof Error) ? error.message : `${error}`; + win?.webContents.send('on-disable-auto-launch-error', err); + } }); // #endregion @@ -1142,8 +748,8 @@ try { tray.setToolTip(toolTip); }); - ipcMain.handle('is-app-image', (event: IpcMainInvokeEvent) => { - win?.webContents.send('on-is-app-image', isAppImage()); + ipcMain.handle('is-portable', (event: IpcMainInvokeEvent) => { + win?.webContents.send('on-is-portable', AppMainProcess.isPortable); }); ipcMain.handle('copy-to-clipboard', (event: IpcMainInvokeEvent, content: string) => { @@ -1157,6 +763,5 @@ try { dialog.showErrorBox('', `${e}`); app.quit(); - // throw e; } diff --git a/app/preload.js b/app/preload.js index 756c026..bac4091 100644 --- a/app/preload.js +++ b/app/preload.js @@ -93,10 +93,10 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('monero-version-error', callback); }, unregisterOnMoneroVersion: () => { - ipcRenderer.removeAllListeners('on-monero-version'); + ipcRenderer.removeAllListeners('monero-version'); }, unregisterOnMoneroVersionError: () => { - ipcRenderer.removeAllListeners('unregister-on-monero-version-error'); + ipcRenderer.removeAllListeners('monero-version-error'); }, downloadMonerod: (downloadUrl, destination) => { ipcRenderer.invoke('download-monerod', downloadUrl, destination); @@ -110,6 +110,9 @@ contextBridge.exposeInMainWorld('electronAPI', { onCheckValidMonerodPath: (callback) => { ipcRenderer.on('on-check-valid-monerod-path', callback); }, + unregisterOnCheckValidMonerodPath: () => { + ipcRenderer.removeAllListeners('on-check-valid-monerod-path'); + }, selectFolder: () => { ipcRenderer.invoke('select-folder') }, @@ -227,14 +230,14 @@ contextBridge.exposeInMainWorld('electronAPI', { unregisterOnDisableAutoLaunchSuccess: () => { ipcRenderer.removeAllListeners('on-disable-auto-launch-success') }, - isAppImage: () => { - ipcRenderer.invoke('is-app-image'); + isPortable: () => { + ipcRenderer.invoke('is-portable'); }, - onIsAppImage: (callback) => { - ipcRenderer.on('on-is-app-image', callback); + onIsPortable: (callback) => { + ipcRenderer.on('on-is-portable', callback); }, - unregisterOnIsAppImage: () => { - ipcRenderer.removeAllListeners('on-is-app-image'); + unregisterIsPortable: () => { + ipcRenderer.removeAllListeners('on-is-portable'); }, isAutoLaunched: () => { ipcRenderer.invoke('is-auto-launched'); diff --git a/app/process/AppChildProcess.ts b/app/process/AppChildProcess.ts new file mode 100644 index 0000000..af50026 --- /dev/null +++ b/app/process/AppChildProcess.ts @@ -0,0 +1,243 @@ +import * as fs from 'fs'; +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { ProcessStats } from './ProcessStats'; + + +const pidusage = require('pidusage'); + +export class AppChildProcess { + + protected _starting: boolean = false; + protected _stopping: boolean = false; + protected _running: boolean = false; + protected _isExe: boolean = true; + + protected _process?: ChildProcessWithoutNullStreams; + + protected _command: string; + protected readonly _args?: string[]; + + 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; + } + + const listeners = this._process.listeners('error'); + + if (listeners.length > 1) { + return; + } + + console.error("Uncaught exeception: "); + console.error(error); + }; + + public get command(): string { + return this._command; + } + + public get args(): string[] { + return this._args ? this._args : []; + } + + public get running(): boolean { + return this._running; + } + + 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); + } + + return v; + } + + 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"); + } + + const exePath = exeComponents[0]; + + if (!fs.existsSync(exePath)) { + throw new Error("Cannot find executable: " + exePath); + } + } + + 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; + } + + this._process.stdout.on('data', cbk); + } + + public onStdErr(callback: (err: string) => void): void { + const cbk = (chunk: any) => callback(`${chunk}`); + + 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; + } + + this._process.on('error', callback); + } + + public onClose(callback: (code: number | null) => void): void { + if (!this._process) { + this._handlers.onclose.push(callback); + return; + } + + this._process.on('close', callback); + } + + 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._running) { + throw new Error("Process already running"); + } + + if (this._isExe) { + this.checkExecutable(); + } + + this._starting = true; + + 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 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(); + }; + + process.once('error', onSpawnError); + process.once('spawn', onSpawn); + }); + + process.once('close', () => { + if (this._stopping) return; + + this._running = false; + this._process = undefined; + }); + + await promise; + } + + 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._running || !this._process) { + throw new Error("Process is not running"); + } + + this._stopping = true; + + const promise = new Promise((resolve) => { + process.on('close', (code: number | null) => { + this._process = undefined; + this._running = false; + this._stopping = false; + resolve(code); + }); + }); + + this._process?.kill(); + + return await promise; + } + + 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"); + } + + return await new Promise((resolve, reject) => { + pidusage(pid, (err: Error | null, stats: ProcessStats) => { + if (err) { + reject(err); + } + else { + resolve(stats); + } + }); + }); + } + +} \ No newline at end of file diff --git a/app/process/AppMainProcess.ts b/app/process/AppMainProcess.ts new file mode 100644 index 0000000..c2f2123 --- /dev/null +++ b/app/process/AppMainProcess.ts @@ -0,0 +1,90 @@ +import AutoLaunch from "../auto-launch"; + +export abstract class AppMainProcess { + + private static autoLaunch: AutoLaunch = new AutoLaunch({ + name: 'monerod-gui', + path: process.execPath, + options: { + launchInBackground: process.argv.includes('--hidden'), + extraArguments: [ + '--auto-launch' + ], + linux: { + comment: 'Monerod GUI startup script', + version: '1.0.1' + } + } + }); + + public static get serve(): boolean { + const args = process.argv.slice(1); + + return args.some(val => val === '--serve'); + } + + public static get autoLaunched(): boolean { + return process.argv.includes('--auto-launch'); + } + + public static get startMinized(): boolean { + return process.argv.includes('--hidden'); + } + + public static get isPortable(): boolean { + return (!!process.env.APPIMAGE) || (!!process.env.PORTABLE_EXECUTABLE_DIR); + } + + public static async isAutoLaunchEnabled(): Promise { + try { + return this.autoLaunch.isEnabled(); + } + catch { + return false; + } + } + + public static async enableAutoLaunch(startMinized: boolean): Promise { + let enabled = await this.isAutoLaunchEnabled(); + + if (enabled) { + throw new Error("Auto launch already enabled"); + } + + this.autoLaunch = new AutoLaunch({ + name: 'monerod-gui', + path: process.execPath, + options: { + launchInBackground: startMinized, + extraArguments: [ + '--auto-launch' + ] + } + }); + + await this.autoLaunch.enable(); + + enabled = await this.isAutoLaunchEnabled(); + + if (!enabled) { + throw new Error("Could not enable auto launch due an unkown error"); + } + } + + public static async disableAutoLaunch(): Promise { + let enabled = await this.isAutoLaunchEnabled(); + + if (!enabled) { + throw new Error("Auto launch already disabled"); + } + + await this.autoLaunch.disable(); + + enabled = await this.isAutoLaunchEnabled(); + + if (enabled) { + throw new Error("Could not disable auto launch due an unknown error"); + } + } + +} \ No newline at end of file diff --git a/app/process/MonerodProcess.ts b/app/process/MonerodProcess.ts new file mode 100644 index 0000000..62a5810 --- /dev/null +++ b/app/process/MonerodProcess.ts @@ -0,0 +1,203 @@ +import { AppChildProcess } from "./AppChildProcess"; + +export class MonerodProcess extends AppChildProcess { + + protected static readonly stdoutPattern: string = '**********************************************************************'; + + 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 static async isValidMonerodPath(monerodPath: string): Promise { + console.log(`MonerodProcess.isValidMonerodPath('${monerodPath}')`); + + if (typeof monerodPath !== 'string' || MonerodProcess.replaceAll(monerodPath, " ", "") == "") { + return false; + } + + try { + MonerodProcess.checkExecutable(monerodPath); + } + catch { + return 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(): '${err.message}'`); + resolve(false); + }); + + proc.onStdErr((err: string) => { + console.log(`MonerodProcess.isValidMonerodPath(): '${err}'`); + resolve(false); + }); + + proc.onStdOut((data: string) => { + if (foundUsage) { + return; + } + + console.log(`MonerodProcess.isValidMonerodPath(): '${data}'`); + + 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); + }); + }); + + try { + await proc.start(); + } + catch(error: any) { + console.log(`MonerodProcess.isValidMonerodPath(): exit code '${error}'`); + } + + return await promise; + } + + public override async start(): Promise { + if (this._isExe) { + const validPath = await MonerodProcess.isValidMonerodPath(this._command); + + if (!validPath) { + throw new Error("Invalid monerod path provided: " + this._command); + } + } + + let message: string = "Starting monerod process"; + + 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 patternPromise = new Promise((resolve, reject) => { + let firstStdout = true; + let timeout: NodeJS.Timeout | undefined = undefined; + + const onStdOut = (out: string) => { + if (firstStdout) { + firstStdout = false; + timeout = setTimeout(() => { + if (this._process && this._process.exitCode == null) { + this._process.kill(); + } + timeout = undefined; + reject(new Error("Timeout out")); + }, 90*1000); + } + + const foundPattern = out.includes(MonerodProcess.stdoutPattern); + + if (!foundPattern) { + return; + } + + if (firstPatternFound) { + if(timeout !== undefined) clearTimeout(timeout); + resolve(); + } + else { + firstPatternFound = true; + } + }; + + this.onStdOut(onStdOut); + }); + + await super.start(); + + if (!this._process || !this._process.pid) { + throw new Error("Monerod process did not start!"); + } + try { + const waitForPattern = this._args ? !this._args.includes('--version') && !this.args.includes('--help') : true; + + if (waitForPattern) await patternPromise; + + 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("proc.onError():"); + console.error(err); + reject(err) + }); + + proc.onStdErr((err: string) => { + console.log("proc.onStdErr()"); + console.error(err); + reject(new Error(err)); + }); + + proc.onStdOut((version: string) => { + console.log("proc.onStdOut():"); + console.log(version); + resolve(version); + }); + }); + + console.log("Before proc.start()"); + await proc.start(); + console.log("After proc.start()"); + + return await promise; + } + +} \ No newline at end of file diff --git a/app/process/ProcessStats.ts b/app/process/ProcessStats.ts new file mode 100644 index 0000000..be794e8 --- /dev/null +++ b/app/process/ProcessStats.ts @@ -0,0 +1,36 @@ +export interface ProcessStats { + /** + * percentage (from 0 to 100*vcore) + */ + cpu: number; + + /** + * bytes + */ + memory: number; + + /** + * PPID + */ + ppid: number; + + /** + * PID + */ + pid: number; + + /** + * ms user + system time + */ + ctime: number; + + /** + * ms since the start of the process + */ + elapsed: number; + + /** + * ms since epoch + */ + timestamp: number; + } \ No newline at end of file diff --git a/app/process/index.ts b/app/process/index.ts new file mode 100644 index 0000000..7658db0 --- /dev/null +++ b/app/process/index.ts @@ -0,0 +1,4 @@ +export { AppMainProcess } from "./AppMainProcess"; +export { ProcessStats } from "./ProcessStats"; +export { AppChildProcess } from "./AppChildProcess"; +export { MonerodProcess } from "./MonerodProcess"; diff --git a/app/utils/BatteryUtils.ts b/app/utils/BatteryUtils.ts new file mode 100644 index 0000000..9f94493 --- /dev/null +++ b/app/utils/BatteryUtils.ts @@ -0,0 +1,39 @@ +import * as os from 'os'; +import { exec, ExecException } from "child_process"; +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 getLevel(): Promise { + try { + return batteryLevel(); + } + catch(error: any) { + console.error(error); + return -1; + } + } +} \ No newline at end of file diff --git a/app/utils/FileUtils.ts b/app/utils/FileUtils.ts new file mode 100644 index 0000000..3026c2b --- /dev/null +++ b/app/utils/FileUtils.ts @@ -0,0 +1,185 @@ +import * as fs from 'fs'; +import * as tar from 'tar'; +import * as path from 'path'; +import * as https from 'https'; +import { createHash } from 'crypto'; + +const AdmZip = require('adm-zip'); +const bz2 = require('unbzip2-stream'); + +export abstract class FileUtils { + + public static async downloadFile(url: string, destinationDir: string, onProgress: (progress: number) => void): Promise { + return new Promise((resolve, reject) => { + const request = (url: string) => { + https.get(url, (response) => { + if (response.statusCode === 200) { + const contentDisposition = response.headers['content-disposition']; + let finalFilename = ''; + + // Estrai il nome del file dall'URL o dal content-disposition + if (contentDisposition && contentDisposition.includes('filename')) { + const match = contentDisposition.match(/filename="(.+)"/); + if (match) { + finalFilename = match[1]; + } + } else { + // Se non c'è content-disposition, prendiamo il nome dall'URL + finalFilename = url.split('/').pop() || 'downloaded-file'; + } + + const destination = `${destinationDir}/${finalFilename}`; + let file: fs.WriteStream; + + try { + file = fs.createWriteStream(destination); + file.on('error', (error: Error) => { + console.log("file error: " + error); + reject(error); + }); + } + catch (error: any) { + reject(error); + return; + } + + const totalBytes = parseInt(response.headers['content-length'] || '0', 10); + let downloadedBytes = 0; + + response.on('data', (chunk) => { + downloadedBytes += chunk.length; + const progress = (downloadedBytes / totalBytes) * 100; + onProgress(progress); // Notifica il progresso + }); + + response.pipe(file); + + file.on('finish', () => { + file.close(() => resolve(finalFilename)); // Restituisci il nome del file finale + }); + } else if (response.statusCode === 301 || response.statusCode === 302) { + // Se è un redirect, effettua una nuova richiesta verso il location header + const newUrl = response.headers.location; + if (newUrl) { + request(newUrl); // Ripeti la richiesta con il nuovo URL + } else { + reject(new Error('Redirection failed without a location header')); + } + } else { + reject(new Error(`Failed to download: ${response.statusCode}`)); + } + }).on('error', (err) => { + reject(err); + }); + }; + + request(url); // Inizia la richiesta + }); + }; + + public static async checkFileHash(filePath: string, hash: string): Promise { + const fileHash = await this.calculateFileHash(filePath); + + return fileHash === hash; + } + + public static calculateFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + const hash = createHash('sha256'); + const fileStream = fs.createReadStream(filePath); + + fileStream.on('data', (data) => { + hash.update(data); + }); + + fileStream.on('end', () => { + resolve(hash.digest('hex')); + }); + + fileStream.on('error', (err) => { + reject(err); + }); + }); + }; + + //#region Extraction + + public static async extractTarBz2(filePath: string, destination: string): Promise { + return await new Promise((resolve, reject) => { + // Crea il file decomprimendo il .bz2 in uno .tar temporaneo + const tarPath = path.join(destination, 'temp.tar'); + const fileStream = fs.createReadStream(filePath); + const decompressedStream = fileStream.pipe(bz2()); + + const writeStream = fs.createWriteStream(tarPath); + + decompressedStream.pipe(writeStream); + + let extractedDir: string = ''; + + writeStream.on('finish', () => { + // Una volta che il file .tar è stato creato, estrailo + tar.extract({ cwd: destination, file: tarPath, onReadEntry: (entry: tar.ReadEntry) => { + if (extractedDir == '') { + const topLevelDir = entry.path.split('/')[0]; + extractedDir = topLevelDir; // Salva la prima directory + } + } }) + .then(() => { + // Elimina il file .tar temporaneo dopo l'estrazione + fs.unlink(tarPath, (err) => { + if (err) reject(err); + else if (extractedDir == '') reject('Extraction failed') + else resolve(extractedDir); + }); + }) + .catch(reject); + }); + + writeStream.on('error', reject); + }); + }; + + public static async extractZip(filePath: string, destination: string): Promise { + return await new Promise((resolve, reject) => { + try { + const zip = new AdmZip(filePath); + + // Ensure destination exists + if (!fs.existsSync(destination)) { + fs.mkdirSync(destination, { recursive: true }); + } + + // Extract the ZIP file + zip.extractAllTo(destination, true); + + // Get the name of the extracted folder + const extractedEntries = zip.getEntries(); + const folderName = extractedEntries[0]?.entryName.split('/')[0]; + + // Ensure folder name exists + if (!folderName) { + reject(new Error("Could not determine the extracted folder name")); + return; + } + + resolve(path.join(destination, folderName)); + } catch (error) { + reject(error); + } + }); + }; + + public static async extract(filePath: string, destination: string): Promise { + if (filePath.endsWith('.zip')) { + return await this.extractZip(filePath, destination); + } + else if (filePath.endsWith('.tar.bz2')) { + return await this.extractTarBz2(filePath, destination); + } + + throw new Error("Unknown file type " + filePath); + } + + //#endregion +} \ No newline at end of file diff --git a/app/utils/NetworkUtils.ts b/app/utils/NetworkUtils.ts new file mode 100644 index 0000000..e95a044 --- /dev/null +++ b/app/utils/NetworkUtils.ts @@ -0,0 +1,71 @@ +import { exec, ExecException } from 'child_process'; +import * as os from 'os'; +const network = require('network'); + +export abstract class NetworkUtils { + public static isConnectedToWiFi(): Promise { + try { + + return new Promise((resolve, reject) => { + network.get_active_interface((err: any | null, obj: { name: string, ip_address: string, mac_address: string, type: string, netmask: string, gateway_ip: string }) => { + if (err) { + console.error(err); + reject(err); + } + else { + resolve(obj.type == 'Wireless'); + } + }) + }); + } + catch(error: any) { + return this.isConnectedToWiFiNative(); + } + } + + private static isConnectedToWiFiNative(): Promise { + return new Promise((resolve, reject) => { + const platform = os.platform(); // Use os to get the platform + + let command = ''; + if (platform === 'win32') { + // Windows: Use 'netsh' command to check the Wi-Fi status + command = 'netsh wlan show interfaces'; + } else if (platform === 'darwin') { + // macOS: Use 'airport' command to check the Wi-Fi status + command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep 'state: running'"; + } else if (platform === 'linux') { + // Linux: Use 'nmcli' to check for Wi-Fi connectivity + command = 'nmcli dev status'; + } else { + resolve(false); // Unsupported platform + } + + // Execute the platform-specific command + if (command) { + exec(command, (error: ExecException | null, stdout: string, stderr: string) => { + if (error) { + console.error(error); + reject(stderr); + resolve(false); // In case of error, assume not connected to Wi-Fi + } else { + // Check if the output indicates a connected status + if (stdout) { + const components: string[] = stdout.split("\n"); + + components.forEach((component: string) => { + if (component.includes('wifi') && !component.includes('--')) { + resolve(true); + } + }); + + resolve(false); + } else { + resolve(false); + } + } + }); + } + }); + } +} \ No newline at end of file diff --git a/app/utils/index.ts b/app/utils/index.ts new file mode 100644 index 0000000..753ebf0 --- /dev/null +++ b/app/utils/index.ts @@ -0,0 +1,3 @@ +export { BatteryUtils } from "./BatteryUtils"; +export { FileUtils } from "./FileUtils"; +export { NetworkUtils } from "./NetworkUtils"; diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index 7e81b2a..a27aa8f 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -249,8 +249,19 @@ export class DaemonService { } public async saveSettings(settings: DaemonSettings, restartDaemon: boolean = true): Promise { + settings.assertValid(); + + if (settings.monerodPath != '') { + const valid = await this.checkValidMonerodPath(settings.monerodPath); + + if (!valid) { + throw new Error("Invalid monerod path provided"); + } + } + const db = await this.openDbPromise; await db.put(this.storeName, { id: 1, ...settings }); + this.onSavedSettings.emit(settings); if (restartDaemon) { @@ -276,6 +287,7 @@ export class DaemonService { const checkPromise = new Promise((resolve) => { window.electronAPI.onCheckValidMonerodPath((event: any, valid: boolean) => { + window.electronAPI.unregisterOnCheckValidMonerodPath(); resolve(valid); }); }); diff --git a/src/app/core/services/electron/electron.service.ts b/src/app/core/services/electron/electron.service.ts index e2ccd8a..46bd2b4 100644 --- a/src/app/core/services/electron/electron.service.ts +++ b/src/app/core/services/electron/electron.service.ts @@ -12,7 +12,7 @@ export class ElectronService { childProcess!: typeof childProcess; fs!: typeof fs; - private _isAppImage?: boolean; + private _isPortable?: boolean; private _isAutoLaunched?: boolean; private _online: boolean = false; private _isProduction: boolean = false; @@ -124,7 +124,7 @@ export class ElectronService { } public async isAutoLaunchEnabled(): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { return false; } @@ -141,7 +141,7 @@ export class ElectronService { } public async enableAutoLaunch(minimized: boolean): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { throw new Error("Cannot enable auto launch"); } @@ -174,7 +174,7 @@ export class ElectronService { public async disableAutoLaunch(): Promise { - if (await this.isAppImage()) { + if (await this.isPortable()) { throw new Error("Cannot disable auto launch"); } @@ -205,21 +205,21 @@ export class ElectronService { await promise; } - public async isAppImage(): Promise { - if (this._isAppImage === undefined) { + public async isPortable(): Promise { + if (this._isPortable === undefined) { const promise = new Promise((resolve) => { - window.electronAPI.onIsAppImage((event: any, value: boolean) => { - window.electronAPI.unregisterOnIsAppImage(); + window.electronAPI.onIsPortable((event: any, value: boolean) => { + window.electronAPI.unregisterIsPortable(); resolve(value); }); }); - window.electronAPI.isAppImage(); + window.electronAPI.isPortable(); - this._isAppImage = await promise; + this._isPortable = await promise; } - return this._isAppImage; + return this._isPortable; } public async selectFile(extensions?: string[]): Promise { diff --git a/src/app/pages/bans/bans.component.html b/src/app/pages/bans/bans.component.html index 59e1b0f..f014979 100644 --- a/src/app/pages/bans/bans.component.html +++ b/src/app/pages/bans/bans.component.html @@ -45,7 +45,7 @@
-
+