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
|
||||
src/**/*.js
|
||||
app/auto-launch/**/*.js
|
||||
app/process/**/*.js
|
||||
app/utils/**/*.js
|
||||
*.js.map
|
||||
|
||||
# dependencies
|
||||
|
|
667
app/main.ts
667
app/main.ts
|
@ -1,95 +1,18 @@
|
|||
import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions,
|
||||
IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor
|
||||
IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor,
|
||||
WebContents,
|
||||
HandlerDetails,
|
||||
Event,
|
||||
WebContentsWillNavigateEventParams
|
||||
} from 'electron';
|
||||
import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import { createHash } from 'crypto';
|
||||
import * as tar from 'tar';
|
||||
import * as os from 'os';
|
||||
import AutoLaunch from './auto-launch';
|
||||
|
||||
const AdmZip = require('adm-zip');
|
||||
const pidusage = require('pidusage');
|
||||
const batteryLevel = require('battery-level');
|
||||
const network = require('network');
|
||||
|
||||
function isOnBatteryPower(): Promise<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');
|
||||
import { AppMainProcess, MonerodProcess } from './process';
|
||||
import { BatteryUtils, FileUtils, NetworkUtils } from './utils';
|
||||
|
||||
app.setName('Monero Daemon');
|
||||
|
||||
let autoLauncher = new AutoLaunch({
|
||||
name: 'monerod-gui',
|
||||
path: process.execPath,
|
||||
options: {
|
||||
extraArguments: [
|
||||
'--auto-launch'
|
||||
],
|
||||
linux: {
|
||||
comment: 'Monerod GUI startup script',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const isAutoLaunched: boolean = process.argv.includes('--auto-launch');
|
||||
const minimized: boolean = process.argv.includes('--hidden');
|
||||
|
||||
let win: BrowserWindow | null = null;
|
||||
let isHidden: boolean = false;
|
||||
let isQuitting: boolean = false;
|
||||
|
@ -102,7 +25,9 @@ const dirname = (__dirname.endsWith(appApp) ? __dirname.replace(appApp, appSrc)
|
|||
|
||||
console.log('dirname: ' + dirname);
|
||||
|
||||
let monerodProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
//let monerodProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
let monerodProcess: MonerodProcess | null = null;
|
||||
|
||||
const iconRelPath: string = 'assets/icons/monero-symbol-on-white-480.png';
|
||||
//const wdwIcon = `${dirname}/${iconRelPath}`;
|
||||
const wdwIcon = path.join(dirname, iconRelPath);
|
||||
|
@ -110,13 +35,6 @@ const wdwIcon = path.join(dirname, iconRelPath);
|
|||
let tray: Tray;
|
||||
let trayMenu: Menu;
|
||||
|
||||
const args = process.argv.slice(1),
|
||||
serve = args.some(val => val === '--serve');
|
||||
|
||||
const isAppImage: () => boolean = () => {
|
||||
return (!!process.env.APPIMAGE) || (!!process.env.PORTABLE_EXECUTABLE_DIR);
|
||||
}
|
||||
|
||||
// #region Window
|
||||
|
||||
function updateTrayMenu(): void {
|
||||
|
@ -217,7 +135,7 @@ function createWindow(): BrowserWindow {
|
|||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
allowRunningInsecureContent: (serve),
|
||||
allowRunningInsecureContent: (AppMainProcess.serve),
|
||||
contextIsolation: true,
|
||||
devTools: !app.isPackaged,
|
||||
sandbox: true
|
||||
|
@ -227,11 +145,11 @@ function createWindow(): BrowserWindow {
|
|||
icon: wdwIcon
|
||||
});
|
||||
|
||||
isHidden = minimized;
|
||||
isHidden = AppMainProcess.startMinized;
|
||||
|
||||
if (!app.isPackaged) win.webContents.openDevTools();
|
||||
|
||||
if (serve) {
|
||||
if (AppMainProcess.serve) {
|
||||
const debug = require('electron-debug');
|
||||
debug();
|
||||
|
||||
|
@ -277,7 +195,7 @@ function createWindow(): BrowserWindow {
|
|||
const createSplashWindow = async (): Promise<BrowserWindow | undefined> => {
|
||||
return undefined;
|
||||
|
||||
if (os.platform() == 'win32' || isAppImage()) {
|
||||
if (os.platform() == 'win32' || AppMainProcess.isPortable) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -320,129 +238,49 @@ const createSplashWindow = async (): Promise<BrowserWindow | undefined> => {
|
|||
|
||||
// #region WiFi
|
||||
|
||||
function isConnectedToWiFi(): Promise<boolean> {
|
||||
try {
|
||||
async function isWifiConnected() {
|
||||
let connected: boolean = false;
|
||||
|
||||
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');
|
||||
}
|
||||
})
|
||||
});
|
||||
try {
|
||||
connected = await NetworkUtils.isConnectedToWiFi();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
connected = false;
|
||||
}
|
||||
|
||||
function isWifiConnected() {
|
||||
isConnectedToWiFi().then((connected: boolean) => {
|
||||
win?.webContents.send('is-wifi-connected-result', connected);
|
||||
}).catch((error: any) => {
|
||||
console.error(error);
|
||||
win?.webContents.send('is-wifi-connected-result', false);
|
||||
});
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region monerod
|
||||
|
||||
function getMonerodVersion(monerodFilePath: string): void {
|
||||
const monerodProcess = spawn(monerodFilePath, [ '--version' ]);
|
||||
|
||||
monerodProcess.on('error', (err: Error) => {
|
||||
win?.webContents.send('monero-version-error', `${err.message}`);
|
||||
});
|
||||
|
||||
monerodProcess.stdout.on('data', (data) => {
|
||||
win?.webContents.send('monero-version', `${data}`);
|
||||
});
|
||||
|
||||
monerodProcess.stderr.on('data', (data) => {
|
||||
win?.webContents.send('monero-version-error', `${data}`);
|
||||
async function getMonerodVersion(monerodFilePath: string): Promise<void> {
|
||||
const proc = new MonerodProcess({
|
||||
monerodCmd: monerodFilePath,
|
||||
isExe: true
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("Before proc.getVersion()");
|
||||
const version = await proc.getVersion();
|
||||
console.log("After proc.getVersion()");
|
||||
win?.webContents.send('monero-version', version);
|
||||
}
|
||||
catch(error: any) {
|
||||
const err = (error instanceof Error) ? error.message : `${error}`;
|
||||
win?.webContents.send('monero-version-error', err);
|
||||
}
|
||||
}
|
||||
|
||||
function checkValidMonerodPath(monerodPath: string): void {
|
||||
let foundUsage: boolean = false;
|
||||
const monerodProcess = spawn(monerodPath, ['--help']);
|
||||
|
||||
monerodProcess.on('error', (err: Error) => {
|
||||
win?.webContents.send('on-check-valid-monerod-path', false);
|
||||
});
|
||||
|
||||
monerodProcess.stderr.on('data', (data) => {
|
||||
win?.webContents.send('on-check-valid-monerod-path', false);
|
||||
});
|
||||
|
||||
monerodProcess.stdout.on('data', (data) => {
|
||||
if (`${data}`.includes('monerod [options|settings] [daemon_command...]')) {
|
||||
foundUsage = true;
|
||||
}
|
||||
});
|
||||
|
||||
monerodProcess.on('close', (code: number) => {
|
||||
win?.webContents.send('on-check-valid-monerod-path', foundUsage);
|
||||
});
|
||||
async function checkValidMonerodPath(monerodPath: string): Promise<void> {
|
||||
const valid = await MonerodProcess.isValidMonerodPath(monerodPath);
|
||||
|
||||
win?.webContents.send('on-check-valid-monerod-path', valid);
|
||||
}
|
||||
|
||||
let moneroFirstStdout: boolean = true;
|
||||
|
||||
function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams {
|
||||
async function startMoneroDaemon(commandOptions: string[]): Promise<MonerodProcess> {
|
||||
const monerodPath = commandOptions.shift();
|
||||
|
||||
if (!monerodPath) {
|
||||
|
@ -457,161 +295,77 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
|
|||
throw new Error("Monerod already started");
|
||||
}
|
||||
|
||||
const message: string = "Starting monerod daemon with options: " + commandOptions.join(" ");
|
||||
|
||||
console.log(message);
|
||||
|
||||
moneroFirstStdout = true;
|
||||
|
||||
commandOptions.push('--non-interactive');
|
||||
|
||||
// Avvia il processo usando spawn
|
||||
monerodProcess = spawn(monerodPath, commandOptions);
|
||||
|
||||
// Gestisci l'output di stdout in streaming
|
||||
monerodProcess.stdout.on('data', (data) => {
|
||||
//console.log(`monerod stdout: ${data}`);
|
||||
const pattern = '**********************************************************************';
|
||||
|
||||
if (moneroFirstStdout && data.includes(pattern)) {
|
||||
win?.webContents.send('monerod-started', true);
|
||||
moneroFirstStdout = false;
|
||||
}
|
||||
|
||||
win?.webContents.send('monero-stdout', `${data}`);
|
||||
// Puoi anche inviare i log all'interfaccia utente tramite IPC
|
||||
monerodProcess = new MonerodProcess({
|
||||
monerodCmd: monerodPath,
|
||||
flags: commandOptions,
|
||||
isExe: true
|
||||
});
|
||||
|
||||
// Gestisci gli errori in stderr
|
||||
monerodProcess.stderr.on('data', (data) => {
|
||||
console.error(`monerod error: ${data}`);
|
||||
|
||||
if (moneroFirstStdout) {
|
||||
win?.webContents.send('monerod-started', false);
|
||||
moneroFirstStdout = false;
|
||||
}
|
||||
monerodProcess.onStdOut((data) => {
|
||||
win?.webContents.send('monero-stdout', `${data}`);
|
||||
});
|
||||
|
||||
monerodProcess.onStdErr((data) => {
|
||||
win?.webContents.send('monero-stderr', `${data}`);
|
||||
});
|
||||
|
||||
// Gestisci la chiusura del processo
|
||||
|
||||
monerodProcess.on('error', (err: Error) => {
|
||||
monerodProcess.onError((err: Error) => {
|
||||
win?.webContents.send('monero-stderr', `${err.message}`);
|
||||
});
|
||||
|
||||
monerodProcess.on('close', (code: number) => {
|
||||
monerodProcess.onClose((_code: number | null) => {
|
||||
const code = _code != null ? _code : -Number.MAX_SAFE_INTEGER;
|
||||
console.log(`monerod exited with code: ${code}`);
|
||||
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
|
||||
win?.webContents.send('monero-close', code);
|
||||
monerodProcess = null;
|
||||
});
|
||||
|
||||
try {
|
||||
await monerodProcess.start();
|
||||
win?.webContents.send('monerod-started', true);
|
||||
}
|
||||
catch(error: any) {
|
||||
win?.webContents.send('monerod-started', false);
|
||||
}
|
||||
|
||||
return monerodProcess;
|
||||
}
|
||||
|
||||
function monitorMonerod(): void {
|
||||
async function monitorMonerod(): Promise<void> {
|
||||
if (!monerodProcess) {
|
||||
win?.webContents.send('on-monitor-monerod-error', 'Monerod not running');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monerodProcess.pid) {
|
||||
win?.webContents.send('on-monitor-monerod-error', 'Unknown monero pid');
|
||||
return;
|
||||
}
|
||||
|
||||
pidusage(monerodProcess.pid, (error: Error | null, stats: Stats) => {
|
||||
if (error) {
|
||||
win?.webContents.send('on-monitor-monerod-error', `${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await monerodProcess.getStats();
|
||||
win?.webContents.send('on-monitor-monerod', stats);
|
||||
});
|
||||
}
|
||||
catch(error: any) {
|
||||
let message: string;
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
}
|
||||
else {
|
||||
message = `${error}`;
|
||||
}
|
||||
|
||||
win?.webContents.send('on-monitor-monerod-error', message);
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Download Utils
|
||||
|
||||
const 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
|
||||
});
|
||||
};
|
||||
|
||||
// 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'), () => {});
|
||||
async function downloadAndVerifyHash(hashUrl: string, fileName: string, filePath: string): Promise<boolean> {
|
||||
const hashFileName = await FileUtils.downloadFile(hashUrl, app.getPath('temp'), () => {});
|
||||
const hashFilePath = `${app.getPath('temp')}/${hashFileName}`;
|
||||
|
||||
// Leggi il file di hash e cerca l'hash corrispondente
|
||||
const hashContent = fs.readFileSync(hashFilePath, 'utf8');
|
||||
const hashLines = hashContent.split('\n');
|
||||
let expectedHash: string | null = null;
|
||||
|
@ -629,106 +383,10 @@ const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath
|
|||
}
|
||||
|
||||
// Verifica l'hash del file scaricato
|
||||
const calculatedHash = await verifyFileHash(`${filePath}/${fileName}`);
|
||||
return calculatedHash === expectedHash;
|
||||
return await FileUtils.checkFileHash(`${filePath}/${fileName}`, expectedHash);
|
||||
};
|
||||
|
||||
// Funzione per verificare l'hash del file
|
||||
const verifyFileHash = (filePath: string): Promise<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
|
||||
|
||||
|
@ -744,6 +402,21 @@ function showNotification(options?: NotificationConstructorOptions): void {
|
|||
new Notification(options).show();
|
||||
}
|
||||
|
||||
function showSecurityWarning(msg: string): void {
|
||||
if (win) {
|
||||
dialog.showMessageBoxSync(win, {
|
||||
type: 'warning',
|
||||
title: 'Security Warning',
|
||||
message: msg
|
||||
});
|
||||
}
|
||||
else {
|
||||
dialog.showErrorBox('Security Warning', msg);
|
||||
}
|
||||
|
||||
console.warn(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
|
@ -767,7 +440,7 @@ try {
|
|||
try {
|
||||
setTimeout(() => {
|
||||
if (splash) splash.close();
|
||||
if (!minimized) {
|
||||
if (!AppMainProcess.startMinized) {
|
||||
win?.show();
|
||||
win?.maximize();
|
||||
}
|
||||
|
@ -804,59 +477,43 @@ try {
|
|||
|
||||
// #region Security
|
||||
|
||||
app.on('web-contents-created', (event, webContents) => {
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
console.warn("Prevented unsafe window creation");
|
||||
app.on('web-contents-created', (event, webContents: WebContents) => {
|
||||
webContents.setWindowOpenHandler((details: HandlerDetails) => {
|
||||
const msg = `Prevented unsafe content: ${details.url}`;
|
||||
showSecurityWarning(msg);
|
||||
console.warn(details);
|
||||
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
webContents.on('will-navigate', (event: Event<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
|
||||
|
||||
ipcMain.handle('is-on-battery-power', (event: IpcMainInvokeEvent) => {
|
||||
const onBattery = powerMonitor.isOnBatteryPower();
|
||||
|
||||
if (!onBattery && os.platform() == 'linux') {
|
||||
isOnBatteryPower().then((value) => {
|
||||
win?.webContents.send('on-is-on-battery-power', value);
|
||||
}).catch((error: any) => {
|
||||
console.error(`${error}`);
|
||||
win?.webContents.send('on-is-on-battery-power', false);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
ipcMain.handle('is-on-battery-power', async (event: IpcMainInvokeEvent) => {
|
||||
const onBattery = await BatteryUtils.isOnBatteryPower();
|
||||
win?.webContents.send('on-is-on-battery-power', onBattery);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
powerMonitor.on('on-ac', () => win?.webContents.send('on-ac'));
|
||||
powerMonitor.on('on-battery', () => win?.webContents.send('on-battery'));
|
||||
|
||||
ipcMain.handle('is-auto-launched', (event: IpcMainInvokeEvent) => {
|
||||
console.debug(event);
|
||||
|
||||
win?.webContents.send('on-is-auto-launched', isAutoLaunched);
|
||||
win?.webContents.send('on-is-auto-launched', AppMainProcess.autoLaunched);
|
||||
});
|
||||
|
||||
ipcMain.handle('quit', (event: IpcMainInvokeEvent) => {
|
||||
ipcMain.handle('quit', async (event: IpcMainInvokeEvent) => {
|
||||
isQuitting = true;
|
||||
|
||||
if (monerodProcess) {
|
||||
await monerodProcess.stop();
|
||||
}
|
||||
|
||||
tray.destroy();
|
||||
win?.close();
|
||||
win?.destroy();
|
||||
|
@ -886,7 +543,7 @@ try {
|
|||
});
|
||||
|
||||
// Scarica il file Monero
|
||||
const fileName = await downloadFile(downloadUrl, destination, (progress) => {
|
||||
const fileName = await FileUtils.downloadFile(downloadUrl, destination, (progress) => {
|
||||
win?.setProgressBar(progress, {
|
||||
mode: 'normal'
|
||||
});
|
||||
|
@ -905,7 +562,7 @@ try {
|
|||
// Estrai il file
|
||||
const fPath = `${destination}/${fileName}`;
|
||||
event.sender.send('download-progress', { progress: 100, status: 'Extracting' });
|
||||
const extractedDir = await extract(fPath, destination);
|
||||
const extractedDir = await FileUtils.extract(fPath, destination);
|
||||
|
||||
event.sender.send('download-progress', { progress: 100, status: 'Download and extraction completed successfully' });
|
||||
event.sender.send('download-progress', { progress: 200, status: os.platform() == 'win32' ? extractedDir : `${destination}/${extractedDir}` });
|
||||
|
@ -926,7 +583,7 @@ try {
|
|||
try {
|
||||
event.sender.send('download-file-progress', { progress: 0, status: 'Starting download' });
|
||||
|
||||
const fileName = await downloadFile(url, destination, (progress) => {
|
||||
const fileName = await FileUtils.downloadFile(url, destination, (progress) => {
|
||||
win?.setProgressBar(progress, {
|
||||
mode: 'normal'
|
||||
});
|
||||
|
@ -1045,87 +702,36 @@ try {
|
|||
|
||||
// #region Auto Launch
|
||||
|
||||
ipcMain.handle('is-auto-launch-enabled', (event: IpcMainInvokeEvent) => {
|
||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
||||
ipcMain.handle('is-auto-launch-enabled', async (event: IpcMainInvokeEvent) => {
|
||||
const enabled = await AppMainProcess.isAutoLaunchEnabled();
|
||||
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) => {
|
||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
||||
if (enabled) {
|
||||
win?.webContents.send('on-enable-auto-launch-error', 'already enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
autoLauncher = new AutoLaunch({
|
||||
name: 'monerod-gui',
|
||||
path: process.execPath,
|
||||
options: {
|
||||
launchInBackground: minimized,
|
||||
extraArguments: [
|
||||
'--auto-launch'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
autoLauncher.enable().then(() => {
|
||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
||||
if (enabled) {
|
||||
ipcMain.handle('enable-auto-launch', async (event: IpcMainInvokeEvent, minimized: boolean) => {
|
||||
try {
|
||||
await AppMainProcess.enableAutoLaunch(minimized);
|
||||
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) {
|
||||
const err = (error instanceof Error) ? error.message : `${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) => {
|
||||
batteryLevel().then((level: number) => {
|
||||
win?.webContents.send('on-get-battery-level', level);
|
||||
}).catch((error: any) => {
|
||||
console.error(error);
|
||||
win?.webContents.send('on-get-battery-level', -1);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.handle('disable-auto-launch', (event: IpcMainInvokeEvent) => {
|
||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
win?.webContents.send('on-disable-auto-launch-error', 'already disabled');
|
||||
return;
|
||||
win?.webContents.send('on-enable-auto-launch-error', err);
|
||||
}
|
||||
});
|
||||
|
||||
autoLauncher.disable().then(() => {
|
||||
autoLauncher.isEnabled().then((enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
ipcMain.handle('get-battery-level', async (event: IpcMainInvokeEvent) => {
|
||||
win?.webContents.send('on-get-battery-level', await BatteryUtils.getLevel());
|
||||
});
|
||||
|
||||
ipcMain.handle('disable-auto-launch', async (event: IpcMainInvokeEvent) => {
|
||||
try {
|
||||
await AppMainProcess.disableAutoLaunch();
|
||||
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}`);
|
||||
});
|
||||
catch(error: any) {
|
||||
const err = (error instanceof Error) ? error.message : `${error}`;
|
||||
win?.webContents.send('on-disable-auto-launch-error', err);
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
@ -1142,8 +748,8 @@ try {
|
|||
tray.setToolTip(toolTip);
|
||||
});
|
||||
|
||||
ipcMain.handle('is-app-image', (event: IpcMainInvokeEvent) => {
|
||||
win?.webContents.send('on-is-app-image', isAppImage());
|
||||
ipcMain.handle('is-portable', (event: IpcMainInvokeEvent) => {
|
||||
win?.webContents.send('on-is-portable', AppMainProcess.isPortable);
|
||||
});
|
||||
|
||||
ipcMain.handle('copy-to-clipboard', (event: IpcMainInvokeEvent, content: string) => {
|
||||
|
@ -1157,6 +763,5 @@ try {
|
|||
dialog.showErrorBox('', `${e}`);
|
||||
|
||||
app.quit();
|
||||
// throw e;
|
||||
}
|
||||
|
||||
|
|
|
@ -93,10 +93,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
ipcRenderer.on('monero-version-error', callback);
|
||||
},
|
||||
unregisterOnMoneroVersion: () => {
|
||||
ipcRenderer.removeAllListeners('on-monero-version');
|
||||
ipcRenderer.removeAllListeners('monero-version');
|
||||
},
|
||||
unregisterOnMoneroVersionError: () => {
|
||||
ipcRenderer.removeAllListeners('unregister-on-monero-version-error');
|
||||
ipcRenderer.removeAllListeners('monero-version-error');
|
||||
},
|
||||
downloadMonerod: (downloadUrl, destination) => {
|
||||
ipcRenderer.invoke('download-monerod', downloadUrl, destination);
|
||||
|
@ -110,6 +110,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
onCheckValidMonerodPath: (callback) => {
|
||||
ipcRenderer.on('on-check-valid-monerod-path', callback);
|
||||
},
|
||||
unregisterOnCheckValidMonerodPath: () => {
|
||||
ipcRenderer.removeAllListeners('on-check-valid-monerod-path');
|
||||
},
|
||||
selectFolder: () => {
|
||||
ipcRenderer.invoke('select-folder')
|
||||
},
|
||||
|
@ -227,14 +230,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
unregisterOnDisableAutoLaunchSuccess: () => {
|
||||
ipcRenderer.removeAllListeners('on-disable-auto-launch-success')
|
||||
},
|
||||
isAppImage: () => {
|
||||
ipcRenderer.invoke('is-app-image');
|
||||
isPortable: () => {
|
||||
ipcRenderer.invoke('is-portable');
|
||||
},
|
||||
onIsAppImage: (callback) => {
|
||||
ipcRenderer.on('on-is-app-image', callback);
|
||||
onIsPortable: (callback) => {
|
||||
ipcRenderer.on('on-is-portable', callback);
|
||||
},
|
||||
unregisterOnIsAppImage: () => {
|
||||
ipcRenderer.removeAllListeners('on-is-app-image');
|
||||
unregisterIsPortable: () => {
|
||||
ipcRenderer.removeAllListeners('on-is-portable');
|
||||
},
|
||||
isAutoLaunched: () => {
|
||||
ipcRenderer.invoke('is-auto-launched');
|
||||
|
|
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> {
|
||||
settings.assertValid();
|
||||
|
||||
if (settings.monerodPath != '') {
|
||||
const valid = await this.checkValidMonerodPath(settings.monerodPath);
|
||||
|
||||
if (!valid) {
|
||||
throw new Error("Invalid monerod path provided");
|
||||
}
|
||||
}
|
||||
|
||||
const db = await this.openDbPromise;
|
||||
await db.put(this.storeName, { id: 1, ...settings });
|
||||
|
||||
this.onSavedSettings.emit(settings);
|
||||
|
||||
if (restartDaemon) {
|
||||
|
@ -276,6 +287,7 @@ export class DaemonService {
|
|||
|
||||
const checkPromise = new Promise<boolean>((resolve) => {
|
||||
window.electronAPI.onCheckValidMonerodPath((event: any, valid: boolean) => {
|
||||
window.electronAPI.unregisterOnCheckValidMonerodPath();
|
||||
resolve(valid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ export class ElectronService {
|
|||
childProcess!: typeof childProcess;
|
||||
fs!: typeof fs;
|
||||
|
||||
private _isAppImage?: boolean;
|
||||
private _isPortable?: boolean;
|
||||
private _isAutoLaunched?: boolean;
|
||||
private _online: boolean = false;
|
||||
private _isProduction: boolean = false;
|
||||
|
@ -124,7 +124,7 @@ export class ElectronService {
|
|||
}
|
||||
|
||||
public async isAutoLaunchEnabled(): Promise<boolean> {
|
||||
if (await this.isAppImage()) {
|
||||
if (await this.isPortable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ export class ElectronService {
|
|||
}
|
||||
|
||||
public async enableAutoLaunch(minimized: boolean): Promise<void> {
|
||||
if (await this.isAppImage()) {
|
||||
if (await this.isPortable()) {
|
||||
throw new Error("Cannot enable auto launch");
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ export class ElectronService {
|
|||
|
||||
|
||||
public async disableAutoLaunch(): Promise<void> {
|
||||
if (await this.isAppImage()) {
|
||||
if (await this.isPortable()) {
|
||||
throw new Error("Cannot disable auto launch");
|
||||
}
|
||||
|
||||
|
@ -205,21 +205,21 @@ export class ElectronService {
|
|||
await promise;
|
||||
}
|
||||
|
||||
public async isAppImage(): Promise<boolean> {
|
||||
if (this._isAppImage === undefined) {
|
||||
public async isPortable(): Promise<boolean> {
|
||||
if (this._isPortable === undefined) {
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
window.electronAPI.onIsAppImage((event: any, value: boolean) => {
|
||||
window.electronAPI.unregisterOnIsAppImage();
|
||||
window.electronAPI.onIsPortable((event: any, value: boolean) => {
|
||||
window.electronAPI.unregisterIsPortable();
|
||||
resolve(value);
|
||||
});
|
||||
});
|
||||
|
||||
window.electronAPI.isAppImage();
|
||||
window.electronAPI.isPortable();
|
||||
|
||||
this._isAppImage = await promise;
|
||||
this._isPortable = await promise;
|
||||
}
|
||||
|
||||
return this._isAppImage;
|
||||
return this._isPortable;
|
||||
}
|
||||
|
||||
public async selectFile(extensions?: string[]): Promise<string> {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
|
||||
<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">
|
||||
<h4><i class="bi bi-check-circle m-2"></i></h4>
|
||||
<div>
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="col-md-4">
|
||||
|
@ -88,7 +88,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<div class="row gy-3">
|
||||
|
||||
|
|
|
@ -482,7 +482,7 @@
|
|||
|
||||
<div class="row g-5 p-2">
|
||||
<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>
|
||||
<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="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>
|
||||
|
||||
<div class="col-md-12">
|
||||
|
@ -38,6 +38,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Path to monerod executable</small>
|
||||
</div>
|
||||
|
@ -105,7 +106,7 @@
|
|||
|
||||
<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="col-md-7 col-lg-12">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
<h4 class="mb-3">Node</h4>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Start monero daemon at login</small>
|
||||
</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>
|
||||
<input class="form-control form-check-input" type="checkbox" role="switch" id="start-minimized" [checked]="currentSettings.startAtLoginMinimized" [(ngModel)]="currentSettings.startAtLoginMinimized" [ngModelOptions]="{standalone: true}">
|
||||
<br>
|
||||
|
@ -147,6 +148,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Specify data directory</small>
|
||||
</div>
|
||||
|
@ -271,6 +273,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Folder where to save updates</small>
|
||||
</div>
|
||||
|
@ -283,7 +286,7 @@
|
|||
|
||||
<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="col-md-7 col-lg-12">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
<h4 class="mb-3">General</h4>
|
||||
<div class="row gy-3">
|
||||
|
||||
|
@ -485,6 +488,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Path to a PEM format private key</small>
|
||||
</div>
|
||||
|
@ -495,6 +499,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Path to a PEM format certificate</small>
|
||||
</div>
|
||||
|
@ -505,6 +510,7 @@
|
|||
<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>
|
||||
<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>
|
||||
<small class="text-body-secondary">Path to file containing concatenated PEM format certificate(s) to replace system CA(s)</small>
|
||||
</div>
|
||||
|
@ -517,7 +523,7 @@
|
|||
|
||||
<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="col-md-7 col-lg-12">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
<h4 class="mb-3">General</h4>
|
||||
<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="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="">
|
||||
|
||||
<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="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="">
|
||||
|
||||
<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="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="">
|
||||
|
||||
<div class="row g-3">
|
||||
|
|
|
@ -85,7 +85,7 @@ export class SettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
public isAppImage: boolean = true;
|
||||
public isPortable: boolean = true;
|
||||
|
||||
public refreshSyncMode(): void {
|
||||
setTimeout(() => {
|
||||
|
@ -128,7 +128,7 @@ export class SettingsComponent {
|
|||
|
||||
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';
|
||||
}
|
||||
|
||||
|
@ -203,7 +203,7 @@ export class SettingsComponent {
|
|||
}
|
||||
|
||||
private async refreshAutoLanch(minimizeChanged: boolean): Promise<void> {
|
||||
if (await this.electronService.isAppImage()) {
|
||||
if (await this.electronService.isPortable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -233,10 +233,6 @@ export class SettingsComponent {
|
|||
this.savingChanges = true;
|
||||
|
||||
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;
|
||||
|
||||
await this.daemonService.saveSettings(this.currentSettings);
|
||||
|
@ -343,6 +339,10 @@ export class SettingsComponent {
|
|||
}
|
||||
}
|
||||
|
||||
public removeMonerodFile(): void {
|
||||
this.currentSettings.monerodPath = '';
|
||||
}
|
||||
|
||||
public async chooseMonerodFile(): Promise<void> {
|
||||
const spec = await this.getMonerodFileSpec();
|
||||
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> {
|
||||
const cert = await this.choosePemFile();
|
||||
|
||||
|
@ -427,6 +431,10 @@ export class SettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
public removeSslCertificate(): void {
|
||||
this.currentSettings.rpcSslCertificate = '';
|
||||
}
|
||||
|
||||
public async selectSslCACertificates(): Promise<void> {
|
||||
const cert = await this.choosePemFile();
|
||||
|
||||
|
@ -437,6 +445,10 @@ export class SettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
public removeSslCACertificates(): void {
|
||||
this.currentSettings.rpcSslCACertificates = '';
|
||||
}
|
||||
|
||||
public async chooseMoneroDownloadPath(): Promise<void> {
|
||||
const folder = await this.electronService.selectFolder();
|
||||
|
||||
|
@ -450,6 +462,10 @@ export class SettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
public removeMoneroDownloadPath(): void {
|
||||
this.currentSettings.downloadUpgradePath = '';
|
||||
}
|
||||
|
||||
public async chooseDataDir(): Promise<void> {
|
||||
const folder = await this.electronService.selectFolder();
|
||||
|
||||
|
@ -462,6 +478,10 @@ export class SettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
public removeDataDir(): void {
|
||||
this.currentSettings.dataDir = '';
|
||||
}
|
||||
|
||||
public chooseXmrigFile(): void {
|
||||
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="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">
|
||||
<h4><i class="bi bi-send-check m-2"></i></h4>
|
||||
<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="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">
|
||||
<h4><i class="bi bi-send-check m-2"></i></h4>
|
||||
<div>
|
||||
|
@ -242,7 +242,7 @@
|
|||
<hr *ngIf="getFeeEstimateSuccess" class="my-4">
|
||||
|
||||
<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="">
|
||||
<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="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">
|
||||
<h4><i class="bi bi-exclamation-triangle m-2"></i></h4>
|
||||
|
@ -346,7 +346,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<form class="needs-validation" novalidate="">
|
||||
<div class="row g-3">
|
||||
|
@ -389,7 +389,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<form class="needs-validation" novalidate="">
|
||||
<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="button" disabled>
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{{ upgrading ? 'Upgrading' : 'Installing' }} monerod
|
||||
{{ upgrading ? 'Upgrading' : 'Installing' }} monerod {{ downloadProgress }}
|
||||
</button>
|
||||
</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 { DaemonService } from '../../core/services/daemon/daemon.service';
|
||||
import { SimpleBootstrapCard } from '../../shared/utils';
|
||||
|
@ -52,7 +52,7 @@ export class VersionComponent implements AfterViewInit {
|
|||
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 = [
|
||||
new NavbarLink('pills-monero-tab', '#pills-monero', 'pills-monero', true, 'Monero')
|
||||
];
|
||||
|
@ -62,7 +62,7 @@ export class VersionComponent implements AfterViewInit {
|
|||
private createCards(): SimpleBootstrapCard[] {
|
||||
return [
|
||||
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)
|
||||
];
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ export class VersionComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
public async load(): Promise<void> {
|
||||
await this.ngZone.run(async () => {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
|
@ -122,6 +123,7 @@ export class VersionComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
public get upgrading(): boolean {
|
||||
|
@ -134,7 +136,13 @@ export class VersionComponent implements AfterViewInit {
|
|||
|
||||
public upgradeSuccess: boolean = false;
|
||||
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 async upgrade(): Promise<void> {
|
||||
|
@ -161,17 +169,23 @@ export class VersionComponent implements AfterViewInit {
|
|||
|
||||
this.upgradeError = '';
|
||||
this.upgradeSuccess = true;
|
||||
|
||||
await this.load();
|
||||
}
|
||||
catch(error: any) {
|
||||
|
||||
console.error(error);
|
||||
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();
|
||||
|
||||
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 {
|
||||
this.upgradeError = err;
|
||||
|
|
|
@ -150,6 +150,24 @@ export class DaemonSettings {
|
|||
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 {
|
||||
//return this.toCommandOptions().join('') == settings.toCommandOptions().join('');
|
||||
return this.deepEqual(this, settings);
|
||||
|
|
|
@ -87,6 +87,7 @@ declare global {
|
|||
onDownloadProgress: (callback: (event: any, progress: { progress: number, status: string }) => void) => void;
|
||||
checkValidMonerodPath: (path: string) => void;
|
||||
onCheckValidMonerodPath: (callback: (event: any, valid: boolean) => void) => void;
|
||||
unregisterOnCheckValidMonerodPath: () => void;
|
||||
unsubscribeOnMonerodStarted: () => void;
|
||||
onMoneroClose: (callback: (event: any, code: number) => void) => void;
|
||||
onMoneroStdout: (callbak: (event: any, out: string) => void) => void;
|
||||
|
@ -120,9 +121,9 @@ declare global {
|
|||
showNotification: (options: NotificationConstructorOptions) => void;
|
||||
quit: () => void;
|
||||
|
||||
isAppImage: () => void;
|
||||
onIsAppImage: (callback: (event: any, value: boolean) => void) => void;
|
||||
unregisterOnIsAppImage: () => void;
|
||||
isPortable: () => void;
|
||||
onIsPortable: (callback: (event: any, value: boolean) => void) => void;
|
||||
unregisterIsPortable: () => void;
|
||||
|
||||
isAutoLaunchEnabled: () => void;
|
||||
onIsAutoLaunchEnabled: (callback: (event: any, enabled: boolean) => void) => void;
|
||||
|
|
Loading…
Reference in a new issue