mirror of
https://github.com/everoddandeven/monerod-gui.git
synced 2024-12-22 11:39:25 +00:00
Refactory, behavior consolidation and minor fixes
Some checks are pending
MacOS - x64 DMG Build / build (20) (push) Waiting to run
Lint Test / build (20) (push) Waiting to run
Linux - AppImage Build / build (20) (push) Waiting to run
Linux - x86_64 RPM Build / build (20) (push) Waiting to run
Linux - x64 DEB Build / build (20) (push) Waiting to run
MacOS Build / build (20) (push) Waiting to run
Windows Build / build (20) (push) Waiting to run
Some checks are pending
MacOS - x64 DMG Build / build (20) (push) Waiting to run
Lint Test / build (20) (push) Waiting to run
Linux - AppImage Build / build (20) (push) Waiting to run
Linux - x86_64 RPM Build / build (20) (push) Waiting to run
Linux - x64 DEB Build / build (20) (push) Waiting to run
MacOS Build / build (20) (push) Waiting to run
Windows Build / build (20) (push) Waiting to run
This commit is contained in:
parent
78cb7f2867
commit
9063c27cd2
24 changed files with 1165 additions and 610 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,6 +9,8 @@
|
||||||
main.js
|
main.js
|
||||||
src/**/*.js
|
src/**/*.js
|
||||||
app/auto-launch/**/*.js
|
app/auto-launch/**/*.js
|
||||||
|
app/process/**/*.js
|
||||||
|
app/utils/**/*.js
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
681
app/main.ts
681
app/main.ts
|
@ -1,95 +1,18 @@
|
||||||
import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions,
|
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';
|
} from 'electron';
|
||||||
import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as https from 'https';
|
|
||||||
import { createHash } from 'crypto';
|
|
||||||
import * as tar from 'tar';
|
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import AutoLaunch from './auto-launch';
|
import { AppMainProcess, MonerodProcess } from './process';
|
||||||
|
import { BatteryUtils, FileUtils, NetworkUtils } from './utils';
|
||||||
const AdmZip = require('adm-zip');
|
|
||||||
const pidusage = require('pidusage');
|
|
||||||
const batteryLevel = require('battery-level');
|
|
||||||
const network = require('network');
|
|
||||||
|
|
||||||
function isOnBatteryPower(): Promise<boolean> {
|
|
||||||
return new Promise<boolean>((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');
|
|
||||||
|
|
||||||
app.setName('Monero Daemon');
|
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 win: BrowserWindow | null = null;
|
||||||
let isHidden: boolean = false;
|
let isHidden: boolean = false;
|
||||||
let isQuitting: boolean = false;
|
let isQuitting: boolean = false;
|
||||||
|
@ -102,7 +25,9 @@ const dirname = (__dirname.endsWith(appApp) ? __dirname.replace(appApp, appSrc)
|
||||||
|
|
||||||
console.log('dirname: ' + dirname);
|
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 iconRelPath: string = 'assets/icons/monero-symbol-on-white-480.png';
|
||||||
//const wdwIcon = `${dirname}/${iconRelPath}`;
|
//const wdwIcon = `${dirname}/${iconRelPath}`;
|
||||||
const wdwIcon = path.join(dirname, iconRelPath);
|
const wdwIcon = path.join(dirname, iconRelPath);
|
||||||
|
@ -110,13 +35,6 @@ const wdwIcon = path.join(dirname, iconRelPath);
|
||||||
let tray: Tray;
|
let tray: Tray;
|
||||||
let trayMenu: Menu;
|
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
|
// #region Window
|
||||||
|
|
||||||
function updateTrayMenu(): void {
|
function updateTrayMenu(): void {
|
||||||
|
@ -217,7 +135,7 @@ function createWindow(): BrowserWindow {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
allowRunningInsecureContent: (serve),
|
allowRunningInsecureContent: (AppMainProcess.serve),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
devTools: !app.isPackaged,
|
devTools: !app.isPackaged,
|
||||||
sandbox: true
|
sandbox: true
|
||||||
|
@ -227,11 +145,11 @@ function createWindow(): BrowserWindow {
|
||||||
icon: wdwIcon
|
icon: wdwIcon
|
||||||
});
|
});
|
||||||
|
|
||||||
isHidden = minimized;
|
isHidden = AppMainProcess.startMinized;
|
||||||
|
|
||||||
if (!app.isPackaged) win.webContents.openDevTools();
|
if (!app.isPackaged) win.webContents.openDevTools();
|
||||||
|
|
||||||
if (serve) {
|
if (AppMainProcess.serve) {
|
||||||
const debug = require('electron-debug');
|
const debug = require('electron-debug');
|
||||||
debug();
|
debug();
|
||||||
|
|
||||||
|
@ -277,7 +195,7 @@ function createWindow(): BrowserWindow {
|
||||||
const createSplashWindow = async (): Promise<BrowserWindow | undefined> => {
|
const createSplashWindow = async (): Promise<BrowserWindow | undefined> => {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
||||||
if (os.platform() == 'win32' || isAppImage()) {
|
if (os.platform() == 'win32' || AppMainProcess.isPortable) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,129 +238,49 @@ const createSplashWindow = async (): Promise<BrowserWindow | undefined> => {
|
||||||
|
|
||||||
// #region WiFi
|
// #region WiFi
|
||||||
|
|
||||||
function isConnectedToWiFi(): Promise<boolean> {
|
async function isWifiConnected() {
|
||||||
|
let connected: boolean = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
connected = await NetworkUtils.isConnectedToWiFi();
|
||||||
return new Promise<boolean>((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) {
|
catch (error: any) {
|
||||||
return isConnectedToWiFiV2();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isConnectedToWiFiV2(): Promise<boolean> {
|
|
||||||
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) => {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
win?.webContents.send('is-wifi-connected-result', false);
|
connected = false;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
win?.webContents.send('is-wifi-connected-result', connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region monerod
|
// #region monerod
|
||||||
|
|
||||||
function getMonerodVersion(monerodFilePath: string): void {
|
async function getMonerodVersion(monerodFilePath: string): Promise<void> {
|
||||||
const monerodProcess = spawn(monerodFilePath, [ '--version' ]);
|
const proc = new MonerodProcess({
|
||||||
|
monerodCmd: monerodFilePath,
|
||||||
monerodProcess.on('error', (err: Error) => {
|
isExe: true
|
||||||
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}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
async function checkValidMonerodPath(monerodPath: string): Promise<void> {
|
||||||
let foundUsage: boolean = false;
|
const valid = await MonerodProcess.isValidMonerodPath(monerodPath);
|
||||||
const monerodProcess = spawn(monerodPath, ['--help']);
|
|
||||||
|
win?.webContents.send('on-check-valid-monerod-path', valid);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let moneroFirstStdout: boolean = true;
|
async function startMoneroDaemon(commandOptions: string[]): Promise<MonerodProcess> {
|
||||||
|
|
||||||
function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams {
|
|
||||||
const monerodPath = commandOptions.shift();
|
const monerodPath = commandOptions.shift();
|
||||||
|
|
||||||
if (!monerodPath) {
|
if (!monerodPath) {
|
||||||
|
@ -456,162 +294,78 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
|
||||||
win?.webContents.send('monero-stderr', error);
|
win?.webContents.send('monero-stderr', error);
|
||||||
throw new Error("Monerod already started");
|
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');
|
commandOptions.push('--non-interactive');
|
||||||
|
|
||||||
// Avvia il processo usando spawn
|
monerodProcess = new MonerodProcess({
|
||||||
monerodProcess = spawn(monerodPath, commandOptions);
|
monerodCmd: monerodPath,
|
||||||
|
flags: commandOptions,
|
||||||
// Gestisci l'output di stdout in streaming
|
isExe: true
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestisci gli errori in stderr
|
monerodProcess.onStdOut((data) => {
|
||||||
monerodProcess.stderr.on('data', (data) => {
|
win?.webContents.send('monero-stdout', `${data}`);
|
||||||
console.error(`monerod error: ${data}`);
|
});
|
||||||
|
|
||||||
if (moneroFirstStdout) {
|
|
||||||
win?.webContents.send('monerod-started', false);
|
|
||||||
moneroFirstStdout = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
monerodProcess.onStdErr((data) => {
|
||||||
win?.webContents.send('monero-stderr', `${data}`);
|
win?.webContents.send('monero-stderr', `${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestisci la chiusura del processo
|
monerodProcess.onError((err: Error) => {
|
||||||
|
|
||||||
monerodProcess.on('error', (err: Error) => {
|
|
||||||
win?.webContents.send('monero-stderr', `${err.message}`);
|
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}`);
|
console.log(`monerod exited with code: ${code}`);
|
||||||
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
|
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
|
||||||
win?.webContents.send('monero-close', code);
|
win?.webContents.send('monero-close', code);
|
||||||
monerodProcess = null;
|
monerodProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await monerodProcess.start();
|
||||||
|
win?.webContents.send('monerod-started', true);
|
||||||
|
}
|
||||||
|
catch(error: any) {
|
||||||
|
win?.webContents.send('monerod-started', false);
|
||||||
|
}
|
||||||
|
|
||||||
return monerodProcess;
|
return monerodProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorMonerod(): void {
|
async function monitorMonerod(): Promise<void> {
|
||||||
if (!monerodProcess) {
|
if (!monerodProcess) {
|
||||||
win?.webContents.send('on-monitor-monerod-error', 'Monerod not running');
|
win?.webContents.send('on-monitor-monerod-error', 'Monerod not running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!monerodProcess.pid) {
|
try {
|
||||||
win?.webContents.send('on-monitor-monerod-error', 'Unknown monero pid');
|
const stats = await monerodProcess.getStats();
|
||||||
return;
|
win?.webContents.send('on-monitor-monerod', stats);
|
||||||
}
|
}
|
||||||
|
catch(error: any) {
|
||||||
|
let message: string;
|
||||||
|
|
||||||
pidusage(monerodProcess.pid, (error: Error | null, stats: Stats) => {
|
if (error instanceof Error) {
|
||||||
if (error) {
|
message = error.message;
|
||||||
win?.webContents.send('on-monitor-monerod-error', `${error}`);
|
}
|
||||||
return;
|
else {
|
||||||
|
message = `${error}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
win?.webContents.send('on-monitor-monerod', stats);
|
win?.webContents.send('on-monitor-monerod-error', message);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Download Utils
|
// #region Download Utils
|
||||||
|
|
||||||
const downloadFile = (url: string, destinationDir: string, onProgress: (progress: number) => void): Promise<string> => {
|
async function downloadAndVerifyHash(hashUrl: string, fileName: string, filePath: string): Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
const hashFileName = await FileUtils.downloadFile(hashUrl, app.getPath('temp'), () => {});
|
||||||
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<boolean> => {
|
|
||||||
//const hashFilePath = path.join(app.getPath('temp'), 'monero_hashes.txt');
|
|
||||||
|
|
||||||
// Scarica il file di hash
|
|
||||||
const hashFileName = await downloadFile(hashUrl, app.getPath('temp'), () => {});
|
|
||||||
const hashFilePath = `${app.getPath('temp')}/${hashFileName}`;
|
const hashFilePath = `${app.getPath('temp')}/${hashFileName}`;
|
||||||
|
|
||||||
// Leggi il file di hash e cerca l'hash corrispondente
|
|
||||||
const hashContent = fs.readFileSync(hashFilePath, 'utf8');
|
const hashContent = fs.readFileSync(hashFilePath, 'utf8');
|
||||||
const hashLines = hashContent.split('\n');
|
const hashLines = hashContent.split('\n');
|
||||||
let expectedHash: string | null = null;
|
let expectedHash: string | null = null;
|
||||||
|
@ -629,106 +383,10 @@ const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica l'hash del file scaricato
|
// Verifica l'hash del file scaricato
|
||||||
const calculatedHash = await verifyFileHash(`${filePath}/${fileName}`);
|
return await FileUtils.checkFileHash(`${filePath}/${fileName}`, expectedHash);
|
||||||
return calculatedHash === expectedHash;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Funzione per verificare l'hash del file
|
// Funzione per verificare l'hash del file
|
||||||
const verifyFileHash = (filePath: string): Promise<string> => {
|
|
||||||
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<string> => {
|
|
||||||
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<string> => {
|
|
||||||
return new Promise<string>((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<string> => {
|
|
||||||
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
|
// #endregion
|
||||||
|
|
||||||
|
@ -744,6 +402,21 @@ function showNotification(options?: NotificationConstructorOptions): void {
|
||||||
new Notification(options).show();
|
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 {
|
try {
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
|
@ -767,7 +440,7 @@ try {
|
||||||
try {
|
try {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (splash) splash.close();
|
if (splash) splash.close();
|
||||||
if (!minimized) {
|
if (!AppMainProcess.startMinized) {
|
||||||
win?.show();
|
win?.show();
|
||||||
win?.maximize();
|
win?.maximize();
|
||||||
}
|
}
|
||||||
|
@ -804,59 +477,43 @@ try {
|
||||||
|
|
||||||
// #region Security
|
// #region Security
|
||||||
|
|
||||||
app.on('web-contents-created', (event, webContents) => {
|
app.on('web-contents-created', (event, webContents: WebContents) => {
|
||||||
webContents.setWindowOpenHandler((details) => {
|
webContents.setWindowOpenHandler((details: HandlerDetails) => {
|
||||||
console.warn("Prevented unsafe window creation");
|
const msg = `Prevented unsafe content: ${details.url}`;
|
||||||
|
showSecurityWarning(msg);
|
||||||
console.warn(details);
|
console.warn(details);
|
||||||
|
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
webContents.on('will-navigate', (event: Event<WebContentsWillNavigateEventParams>, 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
|
// #endregion
|
||||||
|
|
||||||
ipcMain.handle('is-on-battery-power', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('is-on-battery-power', async (event: IpcMainInvokeEvent) => {
|
||||||
const onBattery = powerMonitor.isOnBatteryPower();
|
const onBattery = await BatteryUtils.isOnBatteryPower();
|
||||||
|
win?.webContents.send('on-is-on-battery-power', onBattery);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
powerMonitor.on('on-ac', () => win?.webContents.send('on-ac'));
|
powerMonitor.on('on-ac', () => win?.webContents.send('on-ac'));
|
||||||
powerMonitor.on('on-battery', () => win?.webContents.send('on-battery'));
|
powerMonitor.on('on-battery', () => win?.webContents.send('on-battery'));
|
||||||
|
|
||||||
ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => {
|
||||||
console.debug(event);
|
win?.webContents.send('on-is-auto-launched', AppMainProcess.autoLaunched);
|
||||||
|
|
||||||
win?.webContents.send('on-is-auto-launched', isAutoLaunched);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('quit', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('quit', async (event: IpcMainInvokeEvent) => {
|
||||||
isQuitting = true;
|
isQuitting = true;
|
||||||
|
|
||||||
|
if (monerodProcess) {
|
||||||
|
await monerodProcess.stop();
|
||||||
|
}
|
||||||
|
|
||||||
tray.destroy();
|
tray.destroy();
|
||||||
win?.close();
|
win?.close();
|
||||||
win?.destroy();
|
win?.destroy();
|
||||||
|
@ -886,7 +543,7 @@ try {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scarica il file Monero
|
// Scarica il file Monero
|
||||||
const fileName = await downloadFile(downloadUrl, destination, (progress) => {
|
const fileName = await FileUtils.downloadFile(downloadUrl, destination, (progress) => {
|
||||||
win?.setProgressBar(progress, {
|
win?.setProgressBar(progress, {
|
||||||
mode: 'normal'
|
mode: 'normal'
|
||||||
});
|
});
|
||||||
|
@ -905,7 +562,7 @@ try {
|
||||||
// Estrai il file
|
// Estrai il file
|
||||||
const fPath = `${destination}/${fileName}`;
|
const fPath = `${destination}/${fileName}`;
|
||||||
event.sender.send('download-progress', { progress: 100, status: 'Extracting' });
|
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: 100, status: 'Download and extraction completed successfully' });
|
||||||
event.sender.send('download-progress', { progress: 200, status: os.platform() == 'win32' ? extractedDir : `${destination}/${extractedDir}` });
|
event.sender.send('download-progress', { progress: 200, status: os.platform() == 'win32' ? extractedDir : `${destination}/${extractedDir}` });
|
||||||
|
@ -926,7 +583,7 @@ try {
|
||||||
try {
|
try {
|
||||||
event.sender.send('download-file-progress', { progress: 0, status: 'Starting download' });
|
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, {
|
win?.setProgressBar(progress, {
|
||||||
mode: 'normal'
|
mode: 'normal'
|
||||||
});
|
});
|
||||||
|
@ -1045,87 +702,36 @@ try {
|
||||||
|
|
||||||
// #region Auto Launch
|
// #region Auto Launch
|
||||||
|
|
||||||
ipcMain.handle('is-auto-launch-enabled', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('is-auto-launch-enabled', async (event: IpcMainInvokeEvent) => {
|
||||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
const enabled = await AppMainProcess.isAutoLaunchEnabled();
|
||||||
win?.webContents.send('on-is-auto-launch-enabled', enabled);
|
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('enable-auto-launch', (event: IpcMainInvokeEvent, minimized: boolean) => {
|
ipcMain.handle('enable-auto-launch', async (event: IpcMainInvokeEvent, minimized: boolean) => {
|
||||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
try {
|
||||||
if (enabled) {
|
await AppMainProcess.enableAutoLaunch(minimized);
|
||||||
win?.webContents.send('on-enable-auto-launch-error', 'already enabled');
|
win?.webContents.send('on-enable-auto-launch-success');
|
||||||
return;
|
}
|
||||||
}
|
catch(error: any) {
|
||||||
|
const err = (error instanceof Error) ? error.message : `${error}`;
|
||||||
|
|
||||||
autoLauncher = new AutoLaunch({
|
win?.webContents.send('on-enable-auto-launch-error', err);
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-battery-level', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('get-battery-level', async (event: IpcMainInvokeEvent) => {
|
||||||
batteryLevel().then((level: number) => {
|
win?.webContents.send('on-get-battery-level', await BatteryUtils.getLevel());
|
||||||
win?.webContents.send('on-get-battery-level', level);
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error(error);
|
|
||||||
win?.webContents.send('on-get-battery-level', -1);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('disable-auto-launch', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('disable-auto-launch', async (event: IpcMainInvokeEvent) => {
|
||||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
try {
|
||||||
if (!enabled) {
|
await AppMainProcess.disableAutoLaunch();
|
||||||
win?.webContents.send('on-disable-auto-launch-error', 'already disabled');
|
win?.webContents.send('on-disable-auto-launch-success');
|
||||||
return;
|
}
|
||||||
}
|
catch(error: any) {
|
||||||
|
const err = (error instanceof Error) ? error.message : `${error}`;
|
||||||
autoLauncher.disable().then(() => {
|
win?.webContents.send('on-disable-auto-launch-error', err);
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
@ -1142,8 +748,8 @@ try {
|
||||||
tray.setToolTip(toolTip);
|
tray.setToolTip(toolTip);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('is-app-image', (event: IpcMainInvokeEvent) => {
|
ipcMain.handle('is-portable', (event: IpcMainInvokeEvent) => {
|
||||||
win?.webContents.send('on-is-app-image', isAppImage());
|
win?.webContents.send('on-is-portable', AppMainProcess.isPortable);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('copy-to-clipboard', (event: IpcMainInvokeEvent, content: string) => {
|
ipcMain.handle('copy-to-clipboard', (event: IpcMainInvokeEvent, content: string) => {
|
||||||
|
@ -1157,6 +763,5 @@ try {
|
||||||
dialog.showErrorBox('', `${e}`);
|
dialog.showErrorBox('', `${e}`);
|
||||||
|
|
||||||
app.quit();
|
app.quit();
|
||||||
// throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,10 +93,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
ipcRenderer.on('monero-version-error', callback);
|
ipcRenderer.on('monero-version-error', callback);
|
||||||
},
|
},
|
||||||
unregisterOnMoneroVersion: () => {
|
unregisterOnMoneroVersion: () => {
|
||||||
ipcRenderer.removeAllListeners('on-monero-version');
|
ipcRenderer.removeAllListeners('monero-version');
|
||||||
},
|
},
|
||||||
unregisterOnMoneroVersionError: () => {
|
unregisterOnMoneroVersionError: () => {
|
||||||
ipcRenderer.removeAllListeners('unregister-on-monero-version-error');
|
ipcRenderer.removeAllListeners('monero-version-error');
|
||||||
},
|
},
|
||||||
downloadMonerod: (downloadUrl, destination) => {
|
downloadMonerod: (downloadUrl, destination) => {
|
||||||
ipcRenderer.invoke('download-monerod', downloadUrl, destination);
|
ipcRenderer.invoke('download-monerod', downloadUrl, destination);
|
||||||
|
@ -110,6 +110,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
onCheckValidMonerodPath: (callback) => {
|
onCheckValidMonerodPath: (callback) => {
|
||||||
ipcRenderer.on('on-check-valid-monerod-path', callback);
|
ipcRenderer.on('on-check-valid-monerod-path', callback);
|
||||||
},
|
},
|
||||||
|
unregisterOnCheckValidMonerodPath: () => {
|
||||||
|
ipcRenderer.removeAllListeners('on-check-valid-monerod-path');
|
||||||
|
},
|
||||||
selectFolder: () => {
|
selectFolder: () => {
|
||||||
ipcRenderer.invoke('select-folder')
|
ipcRenderer.invoke('select-folder')
|
||||||
},
|
},
|
||||||
|
@ -227,14 +230,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
unregisterOnDisableAutoLaunchSuccess: () => {
|
unregisterOnDisableAutoLaunchSuccess: () => {
|
||||||
ipcRenderer.removeAllListeners('on-disable-auto-launch-success')
|
ipcRenderer.removeAllListeners('on-disable-auto-launch-success')
|
||||||
},
|
},
|
||||||
isAppImage: () => {
|
isPortable: () => {
|
||||||
ipcRenderer.invoke('is-app-image');
|
ipcRenderer.invoke('is-portable');
|
||||||
},
|
},
|
||||||
onIsAppImage: (callback) => {
|
onIsPortable: (callback) => {
|
||||||
ipcRenderer.on('on-is-app-image', callback);
|
ipcRenderer.on('on-is-portable', callback);
|
||||||
},
|
},
|
||||||
unregisterOnIsAppImage: () => {
|
unregisterIsPortable: () => {
|
||||||
ipcRenderer.removeAllListeners('on-is-app-image');
|
ipcRenderer.removeAllListeners('on-is-portable');
|
||||||
},
|
},
|
||||||
isAutoLaunched: () => {
|
isAutoLaunched: () => {
|
||||||
ipcRenderer.invoke('is-auto-launched');
|
ipcRenderer.invoke('is-auto-launched');
|
||||||
|
|
243
app/process/AppChildProcess.ts
Normal file
243
app/process/AppChildProcess.ts
Normal file
|
@ -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<void> {
|
||||||
|
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<void>((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<number | null> {
|
||||||
|
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<number | null>((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<ProcessStats> {
|
||||||
|
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<ProcessStats>((resolve, reject) => {
|
||||||
|
pidusage(pid, (err: Error | null, stats: ProcessStats) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
app/process/AppMainProcess.ts
Normal file
90
app/process/AppMainProcess.ts
Normal file
|
@ -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<boolean> {
|
||||||
|
try {
|
||||||
|
return this.autoLaunch.isEnabled();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async enableAutoLaunch(startMinized: boolean): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
203
app/process/MonerodProcess.ts
Normal file
203
app/process/MonerodProcess.ts
Normal file
|
@ -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<boolean> {
|
||||||
|
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<boolean>((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<void> {
|
||||||
|
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<void>((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<string> {
|
||||||
|
const proc = new MonerodProcess({
|
||||||
|
monerodCmd: this._command,
|
||||||
|
flags: [ '--version' ],
|
||||||
|
isExe: this._isExe
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = new Promise<string>((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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
app/process/ProcessStats.ts
Normal file
36
app/process/ProcessStats.ts
Normal file
|
@ -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;
|
||||||
|
}
|
4
app/process/index.ts
Normal file
4
app/process/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export { AppMainProcess } from "./AppMainProcess";
|
||||||
|
export { ProcessStats } from "./ProcessStats";
|
||||||
|
export { AppChildProcess } from "./AppChildProcess";
|
||||||
|
export { MonerodProcess } from "./MonerodProcess";
|
39
app/utils/BatteryUtils.ts
Normal file
39
app/utils/BatteryUtils.ts
Normal file
|
@ -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<boolean> {
|
||||||
|
const onBattery = powerMonitor.isOnBatteryPower();
|
||||||
|
|
||||||
|
if (!onBattery && os.platform() == 'linux') {
|
||||||
|
return await new Promise<boolean>((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<number> {
|
||||||
|
try {
|
||||||
|
return batteryLevel();
|
||||||
|
}
|
||||||
|
catch(error: any) {
|
||||||
|
console.error(error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
185
app/utils/FileUtils.ts
Normal file
185
app/utils/FileUtils.ts
Normal file
|
@ -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<string> {
|
||||||
|
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<boolean> {
|
||||||
|
const fileHash = await this.calculateFileHash(filePath);
|
||||||
|
|
||||||
|
return fileHash === hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static calculateFileHash(filePath: string): Promise<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
return await new Promise<string>((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<string> {
|
||||||
|
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
|
||||||
|
}
|
71
app/utils/NetworkUtils.ts
Normal file
71
app/utils/NetworkUtils.ts
Normal file
|
@ -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<boolean> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
return new Promise<boolean>((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<boolean> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
3
app/utils/index.ts
Normal file
3
app/utils/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { BatteryUtils } from "./BatteryUtils";
|
||||||
|
export { FileUtils } from "./FileUtils";
|
||||||
|
export { NetworkUtils } from "./NetworkUtils";
|
|
@ -249,8 +249,19 @@ export class DaemonService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveSettings(settings: DaemonSettings, restartDaemon: boolean = true): Promise<void> {
|
public async saveSettings(settings: DaemonSettings, restartDaemon: boolean = true): Promise<void> {
|
||||||
|
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;
|
const db = await this.openDbPromise;
|
||||||
await db.put(this.storeName, { id: 1, ...settings });
|
await db.put(this.storeName, { id: 1, ...settings });
|
||||||
|
|
||||||
this.onSavedSettings.emit(settings);
|
this.onSavedSettings.emit(settings);
|
||||||
|
|
||||||
if (restartDaemon) {
|
if (restartDaemon) {
|
||||||
|
@ -276,6 +287,7 @@ export class DaemonService {
|
||||||
|
|
||||||
const checkPromise = new Promise<boolean>((resolve) => {
|
const checkPromise = new Promise<boolean>((resolve) => {
|
||||||
window.electronAPI.onCheckValidMonerodPath((event: any, valid: boolean) => {
|
window.electronAPI.onCheckValidMonerodPath((event: any, valid: boolean) => {
|
||||||
|
window.electronAPI.unregisterOnCheckValidMonerodPath();
|
||||||
resolve(valid);
|
resolve(valid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ export class ElectronService {
|
||||||
childProcess!: typeof childProcess;
|
childProcess!: typeof childProcess;
|
||||||
fs!: typeof fs;
|
fs!: typeof fs;
|
||||||
|
|
||||||
private _isAppImage?: boolean;
|
private _isPortable?: boolean;
|
||||||
private _isAutoLaunched?: boolean;
|
private _isAutoLaunched?: boolean;
|
||||||
private _online: boolean = false;
|
private _online: boolean = false;
|
||||||
private _isProduction: boolean = false;
|
private _isProduction: boolean = false;
|
||||||
|
@ -124,7 +124,7 @@ export class ElectronService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isAutoLaunchEnabled(): Promise<boolean> {
|
public async isAutoLaunchEnabled(): Promise<boolean> {
|
||||||
if (await this.isAppImage()) {
|
if (await this.isPortable()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ export class ElectronService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableAutoLaunch(minimized: boolean): Promise<void> {
|
public async enableAutoLaunch(minimized: boolean): Promise<void> {
|
||||||
if (await this.isAppImage()) {
|
if (await this.isPortable()) {
|
||||||
throw new Error("Cannot enable auto launch");
|
throw new Error("Cannot enable auto launch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ export class ElectronService {
|
||||||
|
|
||||||
|
|
||||||
public async disableAutoLaunch(): Promise<void> {
|
public async disableAutoLaunch(): Promise<void> {
|
||||||
if (await this.isAppImage()) {
|
if (await this.isPortable()) {
|
||||||
throw new Error("Cannot disable auto launch");
|
throw new Error("Cannot disable auto launch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,21 +205,21 @@ export class ElectronService {
|
||||||
await promise;
|
await promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isAppImage(): Promise<boolean> {
|
public async isPortable(): Promise<boolean> {
|
||||||
if (this._isAppImage === undefined) {
|
if (this._isPortable === undefined) {
|
||||||
const promise = new Promise<boolean>((resolve) => {
|
const promise = new Promise<boolean>((resolve) => {
|
||||||
window.electronAPI.onIsAppImage((event: any, value: boolean) => {
|
window.electronAPI.onIsPortable((event: any, value: boolean) => {
|
||||||
window.electronAPI.unregisterOnIsAppImage();
|
window.electronAPI.unregisterIsPortable();
|
||||||
resolve(value);
|
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<string> {
|
public async selectFile(extensions?: string[]): Promise<string> {
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<div *ngIf="setBansSuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
<div *ngIf="setBansSuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
||||||
<h4><i class="bi bi-check-circle m-2"></i></h4>
|
<h4><i class="bi bi-check-circle m-2"></i></h4>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-10">
|
<div class="col-md-12 col-lg-10">
|
||||||
<h4 class="mb-3">Set the daemon log level</h4>
|
<h4 class="mb-3">Set the daemon log level</h4>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-10">
|
<div class="col-md-12 col-lg-10">
|
||||||
<h4 class="mb-3">Set the daemon log categories</h4>
|
<h4 class="mb-3">Set the daemon log categories</h4>
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
|
|
||||||
|
|
|
@ -482,7 +482,7 @@
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="cold-md-7 col-lg-12">
|
<div class="cold-md-7 col-lg-12">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
|
|
||||||
<h4 class="mb-3">Submit a mined block to the network</h4>
|
<h4 class="mb-3">Submit a mined block to the network</h4>
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade show active" id="pills-general" role="tabpanel" aria-labelledby="pills-general-tab" tabindex="0">
|
<div class="tab-pane fade show active" id="pills-general" role="tabpanel" aria-labelledby="pills-general-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">Monerod</h4>
|
<h4 class="mb-3">Monerod</h4>
|
||||||
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="general-monerod-path-control" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.monerodPath" readonly>
|
<input id="general-monerod-path-control" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.monerodPath" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMonerodFile()"><i class="bi bi-filetype-exe"></i> Choose executable</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMonerodFile()"><i class="bi bi-filetype-exe"></i> Choose executable</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeMonerodFile()" [disabled]="currentSettings.monerodPath === ''"><i class="bi bi-file-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Path to monerod executable</small>
|
<small class="text-body-secondary">Path to monerod executable</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,7 +106,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-node" role="tabpanel" aria-labelledby="pills-node-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-node" role="tabpanel" aria-labelledby="pills-node-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">Node</h4>
|
<h4 class="mb-3">Node</h4>
|
||||||
|
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
|
@ -121,14 +122,14 @@
|
||||||
<small class="text-body-secondary">Maximum number of connections allowed from the same IP address</small>
|
<small class="text-body-secondary">Maximum number of connections allowed from the same IP address</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!isAppImage" class="form-check form-switch col-md-6">
|
<div *ngIf="!isPortable" class="form-check form-switch col-md-6">
|
||||||
<label for="start-at-login" class="form-check-label">Start at login</label>
|
<label for="start-at-login" class="form-check-label">Start at login</label>
|
||||||
<input class="form-control form-check-input" type="checkbox" role="switch" id="start-at-login" [checked]="currentSettings.startAtLogin" [(ngModel)]="currentSettings.startAtLogin" [ngModelOptions]="{standalone: true}">
|
<input class="form-control form-check-input" type="checkbox" role="switch" id="start-at-login" [checked]="currentSettings.startAtLogin" [(ngModel)]="currentSettings.startAtLogin" [ngModelOptions]="{standalone: true}">
|
||||||
<br>
|
<br>
|
||||||
<small class="text-body-secondary">Start monero daemon at login</small>
|
<small class="text-body-secondary">Start monero daemon at login</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!isAppImage" class="form-check form-switch col-md-6">
|
<div *ngIf="!isPortable" class="form-check form-switch col-md-6">
|
||||||
<label for="start-minimized" class="form-check-label">Start minimized</label>
|
<label for="start-minimized" class="form-check-label">Start minimized</label>
|
||||||
<input class="form-control form-check-input" type="checkbox" role="switch" id="start-minimized" [checked]="currentSettings.startAtLoginMinimized" [(ngModel)]="currentSettings.startAtLoginMinimized" [ngModelOptions]="{standalone: true}">
|
<input class="form-control form-check-input" type="checkbox" role="switch" id="start-minimized" [checked]="currentSettings.startAtLoginMinimized" [(ngModel)]="currentSettings.startAtLoginMinimized" [ngModelOptions]="{standalone: true}">
|
||||||
<br>
|
<br>
|
||||||
|
@ -147,6 +148,7 @@
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="data-dir" type="text" class="form-control form-control-sm" placeholder="" aria-label="Data dir" aria-describedby="basic-addon2" [value]="currentSettings.dataDir" readonly>
|
<input id="data-dir" type="text" class="form-control form-control-sm" placeholder="" aria-label="Data dir" aria-describedby="basic-addon2" [value]="currentSettings.dataDir" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseDataDir()"><i class="bi bi-folder"></i> Choose folder</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseDataDir()"><i class="bi bi-folder"></i> Choose folder</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeDataDir()" [disabled]="currentSettings.dataDir === ''"><i class="bi bi-folder-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Specify data directory</small>
|
<small class="text-body-secondary">Specify data directory</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -271,6 +273,7 @@
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="upgrade-download-path=" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.downloadUpgradePath" readonly>
|
<input id="upgrade-download-path=" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.downloadUpgradePath" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMoneroDownloadPath()"><i class="bi bi-folder"></i> Choose folder</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="chooseMoneroDownloadPath()"><i class="bi bi-folder"></i> Choose folder</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeMoneroDownloadPath()" [disabled]="currentSettings.downloadUpgradePath === ''"><i class="bi bi-folder-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Folder where to save updates</small>
|
<small class="text-body-secondary">Folder where to save updates</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -283,7 +286,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-rpc" role="tabpanel" aria-labelledby="pills-rpc-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-rpc" role="tabpanel" aria-labelledby="pills-rpc-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">General</h4>
|
<h4 class="mb-3">General</h4>
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
|
|
||||||
|
@ -484,7 +487,8 @@
|
||||||
<label for="rpc-ssl-private-key" class="form-label">Private key</label>
|
<label for="rpc-ssl-private-key" class="form-label">Private key</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="rpc-ssl-private-key" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslPrivateKey" readonly>
|
<input id="rpc-ssl-private-key" type="text" class="form-control form-control-sm" placeholder="" aria-label="Monerod path" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslPrivateKey" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslPrivateKey()"><i class="bi bi-filetype-key"></i> Choose file</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslPrivateKey()"><i class="bi bi-filetype-key"></i> Choose file</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeSslPrivateKey()" [disabled]="currentSettings.rpcSslPrivateKey === ''"><i class="bi bi-file-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Path to a PEM format private key</small>
|
<small class="text-body-secondary">Path to a PEM format private key</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -494,7 +498,8 @@
|
||||||
<label for="rpc-ssl-certificate" class="form-label">Certificate</label>
|
<label for="rpc-ssl-certificate" class="form-label">Certificate</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="rpc-ssl-certificate" type="text" class="form-control form-control-sm" placeholder="" aria-label="RPC SSL Certificate" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslCertificate" readonly>
|
<input id="rpc-ssl-certificate" type="text" class="form-control form-control-sm" placeholder="" aria-label="RPC SSL Certificate" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslCertificate" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslCertificate()"><i class="bi bi-postcard"></i> Choose file</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslCertificate()"><i class="bi bi-postcard"></i> Choose file</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeSslCertificate()" [disabled]="currentSettings.rpcSslCertificate === ''"><i class="bi bi-file-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Path to a PEM format certificate</small>
|
<small class="text-body-secondary">Path to a PEM format certificate</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -504,7 +509,8 @@
|
||||||
<label for="rpc-ssl-ca-certificates" class="form-label">CA Certificates</label>
|
<label for="rpc-ssl-ca-certificates" class="form-label">CA Certificates</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input id="rpc-ssl-ca-certificates" type="text" class="form-control form-control-sm" placeholder="" aria-label="RPC SSL CA Certificates" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslCACertificates" readonly>
|
<input id="rpc-ssl-ca-certificates" type="text" class="form-control form-control-sm" placeholder="" aria-label="RPC SSL CA Certificates" aria-describedby="basic-addon2" [value]="currentSettings.rpcSslCACertificates" readonly>
|
||||||
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslCACertificates()"><i class="bi bi-postcard"></i> Choose file</button></span>
|
<span class="input-group-text" id="basic-addon2"><button type="button" class="btn btn-secondary btn-sm" (click)="selectSslCACertificates()"><i class="bi bi-postcard"></i> Choose file</button></span>
|
||||||
|
<span class="input-group-text" id="basic-addon3"><button type="button" class="btn btn-secondary btn-sm" (click)="removeSslCACertificates()" [disabled]="currentSettings.rpcSslCACertificates === ''"><i class="bi bi-file-x"></i> Remove</button></span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-body-secondary">Path to file containing concatenated PEM format certificate(s) to replace system CA(s)</small>
|
<small class="text-body-secondary">Path to file containing concatenated PEM format certificate(s) to replace system CA(s)</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -517,7 +523,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-p2p" role="tabpanel" aria-labelledby="pills-p2p-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-p2p" role="tabpanel" aria-labelledby="pills-p2p-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">General</h4>
|
<h4 class="mb-3">General</h4>
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
|
|
||||||
|
@ -580,7 +586,7 @@
|
||||||
<div class="tab-pane fade" id="pills-blockchain" role="tabpanel" aria-labelledby="pills-blockchain-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-blockchain" role="tabpanel" aria-labelledby="pills-blockchain-tab" tabindex="0">
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
|
|
||||||
<h4 class="mb-3">Network type</h4>
|
<h4 class="mb-3">Network type</h4>
|
||||||
|
@ -772,7 +778,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-mining" role="tabpanel" aria-labelledby="pills-mining-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-mining" role="tabpanel" aria-labelledby="pills-mining-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
@ -819,7 +825,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-logs" role="tabpanel" aria-labelledby="pills-logs-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-logs" role="tabpanel" aria-labelledby="pills-logs-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
|
@ -85,7 +85,7 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAppImage: boolean = true;
|
public isPortable: boolean = true;
|
||||||
|
|
||||||
public refreshSyncMode(): void {
|
public refreshSyncMode(): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -128,7 +128,7 @@ export class SettingsComponent {
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
this.isAppImage = await this.electronService.isAppImage();
|
this.isPortable = await this.electronService.isPortable();
|
||||||
this.networkType = this.currentSettings.mainnet ? 'mainnet' : this.currentSettings.testnet ? 'testnet' : this.currentSettings.stagenet ? 'stagenet' : 'mainnet';
|
this.networkType = this.currentSettings.mainnet ? 'mainnet' : this.currentSettings.testnet ? 'testnet' : this.currentSettings.stagenet ? 'stagenet' : 'mainnet';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ export class SettingsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshAutoLanch(minimizeChanged: boolean): Promise<void> {
|
private async refreshAutoLanch(minimizeChanged: boolean): Promise<void> {
|
||||||
if (await this.electronService.isAppImage()) {
|
if (await this.electronService.isPortable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,11 +232,7 @@ export class SettingsComponent {
|
||||||
|
|
||||||
this.savingChanges = true;
|
this.savingChanges = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.currentSettings.upgradeAutomatically && this.currentSettings.downloadUpgradePath == '') {
|
|
||||||
throw new Error('You must set a download path for monerod updates when enabling automatic upgrade');
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldStartMinimized: boolean = this.originalSettings.startAtLoginMinimized;
|
const oldStartMinimized: boolean = this.originalSettings.startAtLoginMinimized;
|
||||||
|
|
||||||
await this.daemonService.saveSettings(this.currentSettings);
|
await this.daemonService.saveSettings(this.currentSettings);
|
||||||
|
@ -343,6 +339,10 @@ export class SettingsComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeMonerodFile(): void {
|
||||||
|
this.currentSettings.monerodPath = '';
|
||||||
|
}
|
||||||
|
|
||||||
public async chooseMonerodFile(): Promise<void> {
|
public async chooseMonerodFile(): Promise<void> {
|
||||||
const spec = await this.getMonerodFileSpec();
|
const spec = await this.getMonerodFileSpec();
|
||||||
const file = await this.electronService.selectFile(spec.extensions);
|
const file = await this.electronService.selectFile(spec.extensions);
|
||||||
|
@ -417,6 +417,10 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeSslPrivateKey(): void {
|
||||||
|
this.currentSettings.rpcSslPrivateKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
public async selectSslCertificate(): Promise<void> {
|
public async selectSslCertificate(): Promise<void> {
|
||||||
const cert = await this.choosePemFile();
|
const cert = await this.choosePemFile();
|
||||||
|
|
||||||
|
@ -427,6 +431,10 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeSslCertificate(): void {
|
||||||
|
this.currentSettings.rpcSslCertificate = '';
|
||||||
|
}
|
||||||
|
|
||||||
public async selectSslCACertificates(): Promise<void> {
|
public async selectSslCACertificates(): Promise<void> {
|
||||||
const cert = await this.choosePemFile();
|
const cert = await this.choosePemFile();
|
||||||
|
|
||||||
|
@ -436,6 +444,10 @@ export class SettingsComponent {
|
||||||
this.currentSettings.rpcSslCACertificates = cert;
|
this.currentSettings.rpcSslCACertificates = cert;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeSslCACertificates(): void {
|
||||||
|
this.currentSettings.rpcSslCACertificates = '';
|
||||||
|
}
|
||||||
|
|
||||||
public async chooseMoneroDownloadPath(): Promise<void> {
|
public async chooseMoneroDownloadPath(): Promise<void> {
|
||||||
const folder = await this.electronService.selectFolder();
|
const folder = await this.electronService.selectFolder();
|
||||||
|
@ -450,6 +462,10 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeMoneroDownloadPath(): void {
|
||||||
|
this.currentSettings.downloadUpgradePath = '';
|
||||||
|
}
|
||||||
|
|
||||||
public async chooseDataDir(): Promise<void> {
|
public async chooseDataDir(): Promise<void> {
|
||||||
const folder = await this.electronService.selectFolder();
|
const folder = await this.electronService.selectFolder();
|
||||||
|
|
||||||
|
@ -462,6 +478,10 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeDataDir(): void {
|
||||||
|
this.currentSettings.dataDir = '';
|
||||||
|
}
|
||||||
|
|
||||||
public chooseXmrigFile(): void {
|
public chooseXmrigFile(): void {
|
||||||
const input = document.getElementById('general-xmrig-path');
|
const input = document.getElementById('general-xmrig-path');
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@
|
||||||
<div class="tab-pane fade" id="pills-relay-tx" role="tabpanel" aria-labelledby="pills-relay-tx-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-relay-tx" role="tabpanel" aria-labelledby="pills-relay-tx-tab" tabindex="0">
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<div *ngIf="relaySuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
<div *ngIf="relaySuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
||||||
<h4><i class="bi bi-send-check m-2"></i></h4>
|
<h4><i class="bi bi-send-check m-2"></i></h4>
|
||||||
<div>
|
<div>
|
||||||
|
@ -174,7 +174,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-send-raw-tx" role="tabpanel" aria-labelledby="pills-send-raw-tx-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-send-raw-tx" role="tabpanel" aria-labelledby="pills-send-raw-tx-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<div *ngIf="sendRawTxSuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
<div *ngIf="sendRawTxSuccess" class="alert alert-success d-flex align-items-center justify-content-center text-center" role="alert">
|
||||||
<h4><i class="bi bi-send-check m-2"></i></h4>
|
<h4><i class="bi bi-send-check m-2"></i></h4>
|
||||||
<div>
|
<div>
|
||||||
|
@ -242,7 +242,7 @@
|
||||||
<hr *ngIf="getFeeEstimateSuccess" class="my-4">
|
<hr *ngIf="getFeeEstimateSuccess" class="my-4">
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="pills-coinbase-tx-sum" role="tabpanel" aria-labelledby="pills-coinbase-tx-sum-tab" tabindex="0">
|
<div class="tab-pane fade" id="pills-coinbase-tx-sum" role="tabpanel" aria-labelledby="pills-coinbase-tx-sum-tab" tabindex="0">
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
|
|
||||||
<div *ngIf="getCoinbaseTxSumError !== ''" class="alert alert-danger d-flex align-items-center justify-content-center text-center" role="alert">
|
<div *ngIf="getCoinbaseTxSumError !== ''" class="alert alert-danger d-flex align-items-center justify-content-center text-center" role="alert">
|
||||||
<h4><i class="bi bi-exclamation-triangle m-2"></i></h4>
|
<h4><i class="bi bi-exclamation-triangle m-2"></i></h4>
|
||||||
|
@ -346,7 +346,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">Flush a list of transaction IDs</h4>
|
<h4 class="mb-3">Flush a list of transaction IDs</h4>
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
@ -389,7 +389,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-5 p-2">
|
<div class="row g-5 p-2">
|
||||||
<div class="col-md-7 col-lg-12">
|
<div class="col-md-12 col-lg-12">
|
||||||
<h4 class="mb-3">Flush bad transactions / blocks from the cache</h4>
|
<h4 class="mb-3">Flush bad transactions / blocks from the cache</h4>
|
||||||
<form class="needs-validation" novalidate="">
|
<form class="needs-validation" novalidate="">
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
<button *ngIf="!loading && !upgrading && !installing" class="w-100 btn btn-primary btn-lg" type="submit" (click)="upgrade()" [disabled]="buttonDisabled">{{ buttonTitle }}</button>
|
<button *ngIf="!loading && !upgrading && !installing" class="w-100 btn btn-primary btn-lg" type="submit" (click)="upgrade()" [disabled]="buttonDisabled">{{ buttonTitle }}</button>
|
||||||
<button *ngIf="!loading && upgrading || installing" class="w-100 btn btn-primary btn-lg" type="button" disabled>
|
<button *ngIf="!loading && upgrading || installing" class="w-100 btn btn-primary btn-lg" type="button" disabled>
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
{{ upgrading ? 'Upgrading' : 'Installing' }} monerod
|
{{ upgrading ? 'Upgrading' : 'Installing' }} monerod {{ downloadProgress }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AfterViewInit, Component } from '@angular/core';
|
import { AfterViewInit, Component, NgZone } from '@angular/core';
|
||||||
import { NavbarLink } from '../../shared/components/navbar/navbar.model';
|
import { NavbarLink } from '../../shared/components/navbar/navbar.model';
|
||||||
import { DaemonService } from '../../core/services/daemon/daemon.service';
|
import { DaemonService } from '../../core/services/daemon/daemon.service';
|
||||||
import { SimpleBootstrapCard } from '../../shared/utils';
|
import { SimpleBootstrapCard } from '../../shared/utils';
|
||||||
|
@ -52,7 +52,7 @@ export class VersionComponent implements AfterViewInit {
|
||||||
return 'Upgrade';
|
return 'Upgrade';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private daemonData: DaemonDataService, private daemonService: DaemonService, private electronService: ElectronService, private moneroInstaller: MoneroInstallerService) {
|
constructor(private daemonData: DaemonDataService, private daemonService: DaemonService, private electronService: ElectronService, private moneroInstaller: MoneroInstallerService, private ngZone: NgZone) {
|
||||||
this.links = [
|
this.links = [
|
||||||
new NavbarLink('pills-monero-tab', '#pills-monero', 'pills-monero', true, 'Monero')
|
new NavbarLink('pills-monero-tab', '#pills-monero', 'pills-monero', true, 'Monero')
|
||||||
];
|
];
|
||||||
|
@ -62,7 +62,7 @@ export class VersionComponent implements AfterViewInit {
|
||||||
private createCards(): SimpleBootstrapCard[] {
|
private createCards(): SimpleBootstrapCard[] {
|
||||||
return [
|
return [
|
||||||
new SimpleBootstrapCard('GUI Version', this.daemonService.getGuiVersion()),
|
new SimpleBootstrapCard('GUI Version', this.daemonService.getGuiVersion()),
|
||||||
new SimpleBootstrapCard('Current Monerod version', this.currentVersion ? this.currentVersion.fullname : 'Not found', this.loading),
|
new SimpleBootstrapCard('Current Monerod version', this.currentVersion ? this.currentVersion.fullname : 'Not found', this.loading || this.installing),
|
||||||
new SimpleBootstrapCard('Latest Monerod version', this.latestVersion ? this.latestVersion.fullname : 'Error', this.loading)
|
new SimpleBootstrapCard('Latest Monerod version', this.latestVersion ? this.latestVersion.fullname : 'Error', this.loading)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -109,19 +109,21 @@ export class VersionComponent implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
this.loading = true;
|
await this.ngZone.run(async () => {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.settings = await this.daemonService.getSettings();
|
this.settings = await this.daemonService.getSettings();
|
||||||
await this.refreshLatestVersion();
|
await this.refreshLatestVersion();
|
||||||
await this.refreshCurrentVersion();
|
await this.refreshCurrentVersion();
|
||||||
}
|
}
|
||||||
catch(error: any) {
|
catch(error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.cards = this.createErrorCards();
|
this.cards = this.createErrorCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get upgrading(): boolean {
|
public get upgrading(): boolean {
|
||||||
|
@ -134,7 +136,13 @@ export class VersionComponent implements AfterViewInit {
|
||||||
|
|
||||||
public upgradeSuccess: boolean = false;
|
public upgradeSuccess: boolean = false;
|
||||||
public upgradeError: string = '';
|
public upgradeError: string = '';
|
||||||
public downloadProgress: number = 100;
|
|
||||||
|
public get downloadProgress(): string {
|
||||||
|
const ratio = this.moneroInstaller.installing ? this.moneroInstaller.progress.progress : 0;
|
||||||
|
|
||||||
|
return `${ratio <= 100 ? ratio.toFixed(2) : 100} %`;
|
||||||
|
}
|
||||||
|
|
||||||
public downloadStatus : string = '';
|
public downloadStatus : string = '';
|
||||||
|
|
||||||
public async upgrade(): Promise<void> {
|
public async upgrade(): Promise<void> {
|
||||||
|
@ -161,17 +169,23 @@ export class VersionComponent implements AfterViewInit {
|
||||||
|
|
||||||
this.upgradeError = '';
|
this.upgradeError = '';
|
||||||
this.upgradeSuccess = true;
|
this.upgradeSuccess = true;
|
||||||
|
|
||||||
|
await this.load();
|
||||||
}
|
}
|
||||||
catch(error: any) {
|
catch(error: any) {
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.upgradeSuccess = false;
|
this.upgradeSuccess = false;
|
||||||
let err = StringUtils.replaceAll(`${error}`, 'Error: ','');
|
const err = StringUtils.replaceAll(`${error}`, 'Error: ','');
|
||||||
|
|
||||||
if (err.includes('permission denied')) {
|
if (err.includes('permission denied') || err.includes('operation not permitted')) {
|
||||||
const settings = await this.daemonService.getSettings();
|
const settings = await this.daemonService.getSettings();
|
||||||
|
|
||||||
this.upgradeError = 'Cannot download monerod to ' + settings.downloadUpgradePath;
|
this.upgradeError = `Cannot download monerod to <strong>${settings.downloadUpgradePath}</strong> due to insufficient permissions`;
|
||||||
|
|
||||||
|
settings.downloadUpgradePath = '';
|
||||||
|
|
||||||
|
await this.daemonService.saveSettings(settings);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.upgradeError = err;
|
this.upgradeError = err;
|
||||||
|
|
|
@ -150,6 +150,24 @@ export class DaemonSettings {
|
||||||
return this.banList.split('\n');
|
return this.banList.split('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public assertValid(): void {
|
||||||
|
if (this.upgradeAutomatically && this.downloadUpgradePath == '') {
|
||||||
|
throw new Error('You must set a download path for monerod updates when enabling automatic upgrade');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.monerodPath != '' && this.downloadUpgradePath != '') {
|
||||||
|
const separator: '\\' | '/' = this.monerodPath.includes('\\') ? '\\' : '/';
|
||||||
|
const monerodDirComponents = this.monerodPath.split(separator);
|
||||||
|
|
||||||
|
monerodDirComponents.pop();
|
||||||
|
|
||||||
|
if (monerodDirComponents.join(separator) == this.downloadUpgradePath || this.monerodPath == this.downloadUpgradePath) {
|
||||||
|
throw new Error("You must choose a download path other than the monerod path")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public equals(settings: DaemonSettings): boolean {
|
public equals(settings: DaemonSettings): boolean {
|
||||||
//return this.toCommandOptions().join('') == settings.toCommandOptions().join('');
|
//return this.toCommandOptions().join('') == settings.toCommandOptions().join('');
|
||||||
return this.deepEqual(this, settings);
|
return this.deepEqual(this, settings);
|
||||||
|
|
|
@ -87,6 +87,7 @@ declare global {
|
||||||
onDownloadProgress: (callback: (event: any, progress: { progress: number, status: string }) => void) => void;
|
onDownloadProgress: (callback: (event: any, progress: { progress: number, status: string }) => void) => void;
|
||||||
checkValidMonerodPath: (path: string) => void;
|
checkValidMonerodPath: (path: string) => void;
|
||||||
onCheckValidMonerodPath: (callback: (event: any, valid: boolean) => void) => void;
|
onCheckValidMonerodPath: (callback: (event: any, valid: boolean) => void) => void;
|
||||||
|
unregisterOnCheckValidMonerodPath: () => void;
|
||||||
unsubscribeOnMonerodStarted: () => void;
|
unsubscribeOnMonerodStarted: () => void;
|
||||||
onMoneroClose: (callback: (event: any, code: number) => void) => void;
|
onMoneroClose: (callback: (event: any, code: number) => void) => void;
|
||||||
onMoneroStdout: (callbak: (event: any, out: string) => void) => void;
|
onMoneroStdout: (callbak: (event: any, out: string) => void) => void;
|
||||||
|
@ -120,9 +121,9 @@ declare global {
|
||||||
showNotification: (options: NotificationConstructorOptions) => void;
|
showNotification: (options: NotificationConstructorOptions) => void;
|
||||||
quit: () => void;
|
quit: () => void;
|
||||||
|
|
||||||
isAppImage: () => void;
|
isPortable: () => void;
|
||||||
onIsAppImage: (callback: (event: any, value: boolean) => void) => void;
|
onIsPortable: (callback: (event: any, value: boolean) => void) => void;
|
||||||
unregisterOnIsAppImage: () => void;
|
unregisterIsPortable: () => void;
|
||||||
|
|
||||||
isAutoLaunchEnabled: () => void;
|
isAutoLaunchEnabled: () => void;
|
||||||
onIsAutoLaunchEnabled: (callback: (event: any, enabled: boolean) => void) => void;
|
onIsAutoLaunchEnabled: (callback: (event: any, enabled: boolean) => void) => void;
|
||||||
|
|
Loading…
Reference in a new issue