Automatic upgrade and disable sync on wifi implementation

This commit is contained in:
everoddandeven 2024-10-14 23:08:02 +02:00
parent 10f771f890
commit c95017f4d9
10 changed files with 336 additions and 96 deletions

View file

@ -111,6 +111,7 @@ Maybe you only want to execute the application in the browser with hot reload? J
| `npm run ng:serve` | Execute the app in the web browser (DEV mode) |
| `npm run web:build` | Build the app that can be used directly in the web browser. Your built files are in the /dist folder. |
| `npm run electron:local` | Builds your application and start electron locally |
| `npm run electron:local:dev` | Builds your application and start electron locally (DEV MODE) |
| `npm run electron:build` | Builds your application and creates an app consumable based on your operating system |
**Your application is optimised. Only /dist folder and NodeJS dependencies are included in the final bundle.**

View file

@ -1,5 +1,5 @@
import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions } from 'electron';
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as https from 'https';
@ -21,7 +21,13 @@ const args = process.argv.slice(1),
function createWindow(): BrowserWindow {
const size = screen.getPrimaryDisplay().workAreaSize;
const wdwIcon = path.join(__dirname, 'assets/icons/monero-symbol-on-white-480.png');
let dirname = __dirname;
if (dirname.endsWith('/app')) {
dirname = dirname.replace('/app', '/src')
}
const wdwIcon = path.join(dirname, 'assets/icons/monero-symbol-on-white-480.png');
const trayMenuTemplate: MenuItemConstructorOptions[] = [
{
@ -128,14 +134,19 @@ function createWindow(): BrowserWindow {
return win;
}
function isWifiConnected() {
function isWifiConnectedOld() {
console.log("isWifiConnected()");
const networkInterfaces = os.networkInterfaces();
console.log(`isWifiConnected(): network interfaces ${networkInterfaces}`);
console.log(networkInterfaces);
for (const interfaceName in networkInterfaces) {
const networkInterface = networkInterfaces[interfaceName];
if (networkInterface) {
for (const network of networkInterface) {
console.log(network);
if (network.family === 'IPv4' && !network.internal && network.mac !== '00:00:00:00:00:00') {
if (interfaceName.toLowerCase().includes('wifi') || interfaceName.toLowerCase().includes('wlan')) {
return true;
@ -147,6 +158,62 @@ function isWifiConnected() {
return false;
}
function isConnectedToWiFi(): 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");
console.log(components);
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);
win?.webContents.send('is-wifi-connected-result', false);
});
}
function getMonerodVersion(monerodFilePath: string): void {
const monerodProcess = spawn(monerodFilePath, [ '--version' ]);
@ -159,6 +226,8 @@ function getMonerodVersion(monerodFilePath: string): void {
})
}
let moneroFirstStdout: boolean = true;
function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams {
const monerodPath = commandOptions.shift();
@ -169,25 +238,40 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
console.log("Starting monerod daemon with options: " + commandOptions.join(" "));
moneroFirstStdout = true;
// Avvia il processo usando spawn
const 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
});
// Gestisci gli errori in stderr
monerodProcess.stderr.on('data', (data) => {
//console.error(`monerod stderr: ${data}`);
console.error(`monerod error: ${data}`);
if (moneroFirstStdout) {
win?.webContents.send('monerod-started', false);
moneroFirstStdout = false;
}
win?.webContents.send('monero-stderr', `${data}`);
});
// Gestisci la chiusura del processo
monerodProcess.on('close', (code) => {
console.log(`monerod chiuso con codice: ${code}`);
monerodProcess.on('close', (code: number) => {
console.log(`monerod exited with code: ${code}`);
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
win?.webContents.send('monero-close', code);
});
@ -421,7 +505,8 @@ try {
});
ipcMain.handle('is-wifi-connected', async (event) => {
win?.webContents.send('is-wifi-connected-result', isWifiConnected());
isWifiConnected();
//win?.webContents.send('is-wifi-connected-result', isWifiConnected());
});
ipcMain.handle('get-os-type', (event) => {

View file

@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
startMonerod: (args) => {
ipcRenderer.invoke('start-monerod', args);
},
onMonerodStarted: (callback) => {
ipcRenderer.on('monerod-started', callback);
},
onMoneroStdout: (callback) => {
ipcRenderer.on('monero-stdout', callback);
},

View file

@ -38,6 +38,7 @@
"electron:serve-tsc": "tsc -p tsconfig.serve.json",
"electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && electron . --serve",
"electron:local": "npm run build:prod && electron .",
"electron:local:dev": "npm run build:dev && electron .",
"electron:build": "npm run build:prod && electron-builder build --publish=never",
"test": "ng test --watch=false",
"test:watch": "ng test",

View file

@ -60,7 +60,7 @@ export class DaemonDataService {
private _txPoolBacklog: TxBacklogEntry[] = [];
private _gettingTxPoolBackLog: boolean = false;
public readonly syncStart: EventEmitter<void> = new EventEmitter<void>();
public readonly syncStart: EventEmitter<{ first: boolean }> = new EventEmitter<{ first: boolean }>();
public readonly syncEnd: EventEmitter<void> = new EventEmitter<void>();
public readonly syncError: EventEmitter<Error> = new EventEmitter<Error>();
@ -91,7 +91,7 @@ export class DaemonDataService {
}
public get running(): boolean {
return this._daemonRunning;
return this._daemonRunning
}
public get starting(): boolean {
@ -239,11 +239,17 @@ export class DaemonDataService {
throw new Error("Loop already started");
}
this._firstRefresh = true;
this.refresh().then(() => {
this.refreshInterval = setInterval(() => {
this.refresh().then().catch((error: any) => {
console.error(error);
});
},this.refreshTimeoutMs);
}).catch((error: any) => {
console.error(error);
this._refreshing = false;
});
}
private stopLoop(): void {
@ -320,7 +326,31 @@ export class DaemonDataService {
}
this._refreshing = true;
this.syncStart.emit();
const settings = await this.daemonService.getSettings();
const updateInfo = await this.daemonService.checkUpdate()
if (updateInfo.update) {
await this.daemonService.upgrade();
return;
}
const syncAlreadyDisabled = this.daemonService.settings.noSync;
const wifiConnected = await this.daemonService.isWifiConnected();
if (!settings.noSync && !syncAlreadyDisabled && !settings.syncOnWifi && wifiConnected) {
console.log("Disabling sync ...");
await this.daemonService.disableSync();
}
else if (!settings.noSync && syncAlreadyDisabled && !settings.syncOnWifi && !wifiConnected) {
console.log("Enabling sync ...");
await this.daemonService.enableSync();
}
this.syncStart.emit({ first: this._firstRefresh });
try {
const firstRefresh = this._firstRefresh;
@ -337,6 +367,11 @@ export class DaemonDataService {
this._daemonInfo = await this.daemonService.getInfo();
this._gettingDaemonInfo = false;
if (this.daemonService.settings.upgradeAutomatically && this._daemonInfo.updateAvailable) {
await this.daemonService.upgrade();
return;
}
this._gettingSyncInfo = true;
this.syncInfoRefreshStart.emit();
this._syncInfo = await this.daemonService.syncInfo();

View file

@ -79,6 +79,7 @@ import { DaemonSettings } from '../../../../common/DaemonSettings';
import { MethodNotFoundError } from '../../../../common/error/MethodNotFoundError';
import { openDB, IDBPDatabase } from "idb"
import { PeerInfo, TxPool } from '../../../../common';
import { MoneroInstallerService } from '../monero-installer/monero-installer.service';
@Injectable({
providedIn: 'root'
@ -100,6 +101,9 @@ export class DaemonService {
public stopping: boolean = false;
public starting: boolean = false;
public restarting: boolean = false;
public disablingSync: boolean = false;
public enablingSync: boolean = false;
public readonly onDaemonStatusChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
public readonly onDaemonStopStart: EventEmitter<void> = new EventEmitter<void>();
public readonly onDaemonStopEnd: EventEmitter<void> = new EventEmitter<void>();
@ -111,9 +115,10 @@ export class DaemonService {
"Access-Control-Allow-Methods": 'POST,GET' // this states the allowed methods
};
constructor(private httpClient: HttpClient, private electronService: ElectronService) {
constructor(private installer: MoneroInstallerService, private httpClient: HttpClient, private electronService: ElectronService) {
this.openDbPromise = this.openDatabase();
this.settings = this.loadSettings();
this.settings = new DaemonSettings();
const wdw = (window as any);
if (this.electronService.isElectron) {
@ -132,6 +137,85 @@ export class DaemonService {
}
}
public async isWifiConnected(): Promise<boolean> {
try {
return new Promise<boolean>((resolve, reject) => {
try {
window.electronAPI.onIsWifiConnectedResponse((event: any, connected: boolean) => {
console.debug(event);
resolve(connected);
});
window.electronAPI.isWifiConnected();
}
catch(error: any) {
reject(error);
}
});
}
catch(error: any) {
console.error(error);
}
return false;
}
public async disableSync(): Promise<void> {
this.disablingSync = true;
try {
const running: boolean = await this.isRunning();
if (!running) {
throw new Error("Daemon not running");
}
if (this.settings.noSync) {
throw new Error("Daemon already not syncing");
}
await this.stopDaemon();
this.settings.noSync = true;
await this.startDaemon(this.settings);
}
catch(error: any) {
console.error(error);
}
this.disablingSync = false;
}
public async enableSync(): Promise<void> {
this.enablingSync = true;
try {
const running: boolean = await this.isRunning();
if (!running) {
throw new Error("Daemon not running");
}
if (!this.settings.noSync) {
throw new Error("Daemon already not syncing");
}
await this.stopDaemon();
this.settings.noSync = false;
await this.startDaemon(this.settings);
}
catch(error: any) {
console.error(error);
}
this.enablingSync = false;
}
private onClose(): void {
this.daemonRunning = false;
this.stopping = false;
@ -156,7 +240,6 @@ export class DaemonService {
public async saveSettings(settings: DaemonSettings, restartDaemon: boolean = true): Promise<void> {
const db = await this.openDbPromise;
await db.put(this.storeName, { id: 1, ...settings });
this.settings = settings;
if (restartDaemon) {
const running = await this.isRunning();
@ -178,14 +261,12 @@ export class DaemonService {
const db = await this.openDbPromise;
const result = await db.get(this.storeName, 1);
if (result) {
this.settings = DaemonSettings.parse(result);
return DaemonSettings.parse(result);
}
else
{
this.settings = new DaemonSettings();
return new DaemonSettings();
}
return this.settings;
}
public async deleteSettings(): Promise<void> {
@ -193,32 +274,7 @@ export class DaemonService {
await db.delete(this.storeName, 1);
}
private loadSettings(): DaemonSettings {
/*
const args = [
'--testnet',
'--fast-block-sync', '1',
'--prune-blockchain',
'--sync-pruned-blocks',
'--confirm-external-bind',
'--max-concurrency', '1',
'--log-level', '1',
'--rpc-access-control-origins=*'
];
*/
const settings = new DaemonSettings();
settings.testnet = true;
settings.fastBlockSync = true;
settings.pruneBlockchain = true;
settings.syncPrunedBlocks = true;
settings.confirmExternalBind = true;
settings.logLevel = 1;
settings.rpcAccessControlOrigins = "*";
return settings;
}
private raiseRpcError(error: RpcError): void {
if (error.code == -9) {
throw new CoreIsBusyError();
}
@ -229,7 +285,6 @@ export class DaemonService {
{
throw new Error(error.message);
}
}
private async delay(ms: number = 0): Promise<void> {
@ -281,7 +336,8 @@ export class DaemonService {
}
}
public async startDaemon(): Promise<void> {
public async startDaemon(customSettings?: DaemonSettings): Promise<void> {
await new Promise<void>(async (resolve, reject) => {
if (await this.isRunning()) {
console.warn("Daemon already running");
return;
@ -290,27 +346,55 @@ export class DaemonService {
this.starting = true;
console.log("Starting daemon");
const settings = await this.getSettings();
if (this.electronService.ipcRenderer) this.electronService.ipcRenderer.send('start-monerod', settings.toCommandOptions());
else {
(window as any).electronAPI.startMonerod(settings.toCommandOptions());
this.settings = customSettings ? customSettings : await this.getSettings();
if (!this.settings.noSync && !this.settings.syncOnWifi) {
const wifiConnected = await this.isWifiConnected();
if (wifiConnected) {
console.log("Disabling sync ...");
this.settings.noSync = true;
}
}
else if (!this.settings.noSync && !this.settings.syncOnWifi) {
const wifiConnected = await this.isWifiConnected();
if (!wifiConnected) {
console.log("Enabling sync ...");
this.settings.noSync = false;
}
}
await this.delay(3000);
window.electronAPI.onMonerodStarted((event: any, started: boolean) => {
console.debug(event);
if (await this.isRunning(true)) {
if (started) {
console.log("Daemon started");
this.onDaemonStatusChanged.emit(true);
resolve();
}
else
{
else {
console.log("Daemon not started");
this.onDaemonStatusChanged.emit(false);
reject('Could not start daemon');
}
})
window.electronAPI.startMonerod(this.settings.toCommandOptions());
});
this.starting = false;
const isRunning: boolean = await this.isRunning(true);
if (!isRunning) {
throw new Error("Daemon started but not running");
}
}
public async restartDaemon(): Promise<void> {
@ -965,6 +1049,25 @@ export class DaemonService {
return response.height;
}
public async upgrade(): Promise<void> {
const settings = await this.getSettings();
if (settings.upgradeAutomatically) {
throw new Error('Monero Daemon will upgrade automatically');
}
if (settings.downloadUpgradePath == '') {
throw new Error("Download path not configured");
}
//const downloadUrl = 'https://downloads.getmonero.org/cli/linux64'; // Cambia in base al sistema
const destination = settings.downloadUpgradePath; // Aggiorna con il percorso desiderato
const moneroFolder = await this.installer.downloadMonero(destination);
settings.monerodPath = `${moneroFolder}/monerod`;
await this.saveSettings(settings);
}
public async update(command: 'check' | 'download', path: string = ''): Promise<UpdateInfo> {
const response = await this.callRpc(new UpdateRequest(command, path));

View file

@ -1,15 +1,16 @@
import { Component, AfterViewInit, NgZone } from '@angular/core';
import { Component, AfterViewInit, NgZone, OnDestroy } from '@angular/core';
import { Peer } from '../../../common/Peer';
import { NavbarLink } from '../../shared/components/navbar/navbar.model';
import { NavbarService } from '../../shared/components/navbar/navbar.service';
import { DaemonService, DaemonDataService } from '../../core/services';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.scss']
})
export class DetailComponent implements AfterViewInit {
export class DetailComponent implements AfterViewInit, OnDestroy {
public get daemonRunning(): boolean {
return this.daemonData.running;
@ -107,6 +108,8 @@ export class DetailComponent implements AfterViewInit {
public cards: Card[];
private subscriptions: Subscription[] = [];
constructor(
private daemonService: DaemonService,
private navbarService: NavbarService,
@ -121,7 +124,18 @@ export class DetailComponent implements AfterViewInit {
this.cards = this.createCards();
this.daemonData.syncInfoRefreshEnd.subscribe(() => {
this.subscriptions.push(this.daemonData.syncStart.subscribe((info) => {
if(!info.first) {
return;
}
this.ngZone.run(() => {
this.cards = this.createLoadingCards();
});
}));
this.subscriptions.push(this.daemonData.syncInfoRefreshEnd.subscribe(() => {
const $table = $('#table');
//$table.bootstrapTable({});
$table.bootstrapTable('refreshOptions', {
@ -135,10 +149,11 @@ export class DetailComponent implements AfterViewInit {
}
this.cards = this.createCards();
})
}));
}
ngAfterViewInit(): void {
public ngAfterViewInit(): void {
console.log('DetailComponent AFTER VIEW INIT');
this.navbarService.setLinks(this.navbarLinks);
this.ngZone.run(() => {
@ -155,6 +170,11 @@ export class DetailComponent implements AfterViewInit {
});
}
public ngOnDestroy(): void {
this.subscriptions.forEach((sub) => sub.unsubscribe());
this.subscriptions = [];
}
private createLoadingCards(): Card[] {
return [
new Card('Connection Status', this.connectionStatus, true),

View file

@ -4,6 +4,7 @@ import { DaemonService } from '../../core/services/daemon/daemon.service';
import { SimpleBootstrapCard } from '../../shared/utils';
import { DaemonVersion } from '../../../common/DaemonVersion';
import { DaemonDataService, ElectronService, MoneroInstallerService } from '../../core/services';
import { DaemonSettings } from '../../../common';
@Component({
selector: 'app-version',
@ -15,6 +16,7 @@ export class VersionComponent implements AfterViewInit {
public cards: SimpleBootstrapCard[];
public currentVersion?: DaemonVersion;
public latestVersion?: DaemonVersion;
public settings: DaemonSettings = new DaemonSettings();
public get buttonDisabled(): boolean {
const title = this.buttonTitle;
@ -23,7 +25,7 @@ export class VersionComponent implements AfterViewInit {
return false;
}
const configured = this.daemonService.settings.monerodPath != '';
const configured = this.settings.monerodPath != '';
const updateAvailable = this.daemonData.info ? this.daemonData.info.updateAvailable : false;
if (title == 'Upgrade' && configured && updateAvailable) {
@ -40,7 +42,7 @@ export class VersionComponent implements AfterViewInit {
return 'Upgrade';
}
const notConfigured = this.daemonService.settings.monerodPath == '';
const notConfigured = this.settings.monerodPath == '';
if (notConfigured) {
return 'Install';
@ -87,6 +89,7 @@ export class VersionComponent implements AfterViewInit {
}
public async load(): Promise<void> {
this.settings = await this.daemonService.getSettings();
const isElectron = this.electronService.isElectron || (window as any).electronAPI != null;
const version = await this.daemonService.getVersion(isElectron);
const latestVersion = await this.daemonService.getLatestVersion();
@ -109,22 +112,7 @@ export class VersionComponent implements AfterViewInit {
this.upgrading = true;
try {
const settings = await this.daemonService.getSettings();
if (settings.upgradeAutomatically) {
throw new Error('Monero Daemon will upgrade automatically');
}
if (settings.downloadUpgradePath == '') {
throw new Error("Download path not configured");
}
//const downloadUrl = 'https://downloads.getmonero.org/cli/linux64'; // Cambia in base al sistema
const destination = settings.downloadUpgradePath; // Aggiorna con il percorso desiderato
const moneroFolder = await this.moneroInstaller.downloadMonero(destination);
settings.monerodPath = `${moneroFolder}/monerod`;
await this.daemonService.saveSettings(settings);
await this.daemonService.upgrade();
this.upgradeError = '';
this.upgradeSuccess = true;

View file

@ -58,10 +58,14 @@ import 'bootstrap-table';
declare global {
interface Window {
electronAPI: {
startMonerod: (args: string[]) => void;
startMonerod: (options: string[]) => void;
onMonerodStarted: (callback: (event: any, started: boolean) => void) => void;
isWifiConnected: () => void;
onIsWifiConnectedResponse: (callback: (event: any, connected: boolean) => void) => void;
getOsType: () => void;
gotOsType: (callback: (event: any, osType: { platform: string, arch: string }) => void) => void;
quit: () => void;
};
}
}