Disable run on battery feature

This commit is contained in:
argenius 2024-11-10 21:40:33 +01:00
parent 897cc00bb7
commit 9ac4cef1c0
14 changed files with 315 additions and 66 deletions

View file

@ -54,6 +54,9 @@ jobs:
- name: Build the app
run: npm run electron:build
- name: List artifacts
run: ls release/
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:

View file

@ -1,5 +1,5 @@
import { app, BrowserWindow, ipcMain, screen, dialog, Tray, Menu, MenuItemConstructorOptions,
IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard
IpcMainInvokeEvent, Notification, NotificationConstructorOptions, clipboard, powerMonitor
} from 'electron';
import { ChildProcessWithoutNullStreams, exec, ExecException, spawn } from 'child_process';
import * as path from 'path';
@ -782,6 +782,13 @@ try {
})
// #endregion
ipcMain.handle('is-on-battery-power', (event: IpcMainInvokeEvent) => {
win?.webContents.send('on-is-on-battery-power', powerMonitor.isOnBatteryPower());
});
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);

View file

@ -8,6 +8,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
onGetBatteryLevel: (callback) => {
ipcRenderer.on('on-get-battery-level', callback);
},
isOnBatteryPower: () => {
ipcRenderer.invoke('is-on-battery-power');
},
onIsOnBatteryPower: (callback) => {
ipcRenderer.on('on-is-on-battery-power', callback);
},
unregisterOnIsOnBatteryPower: () => {
ipcRenderer.removeAllListeners('on-is-on-battery-power');
},
onAc: (callback) => {
ipcRenderer.on('on-ac', callback);
},
onBattery: (callback) => {
ipcRenderer.on('on-battery', callback);
},
unregisterOnGetBatteryLevel: () => {
ipcRenderer.removeAllListeners('on-get-battery-level');
},

View file

@ -69,6 +69,11 @@ export class DaemonDataService {
private _txPoolStats?: TxPoolStats;
private _gettingTxPoolStats: boolean = false;
private _runningOnBattery?: boolean;
public get runningOnBattery(): boolean {
return this._runningOnBattery === true;
}
public readonly syncStart: EventEmitter<{ first: boolean }> = new EventEmitter<{ first: boolean }>();
public readonly syncEnd: EventEmitter<void> = new EventEmitter<void>();
public readonly syncError: EventEmitter<any> = new EventEmitter<any>();
@ -92,8 +97,12 @@ export class DaemonDataService {
this.stopLoop();
}
});
});
this.electronService.onAcPower.subscribe(() => this._runningOnBattery = false);
this.electronService.onBatteryPower.subscribe(() => this._runningOnBattery = true);
this.electronService.isOnBatteryPower().then((value: boolean) => this._runningOnBattery = value).catch((error: any) => console.error(error));
}
public get initializing(): boolean {
@ -350,6 +359,8 @@ export class DaemonDataService {
}
public batteryLevel: number = 0;
public syncDisabledByWifiPolicy: boolean = false;
public syncDisabledByPeriodPolicy: boolean = false;
@ -359,11 +370,30 @@ export class DaemonDataService {
return;
}
if (this._runningOnBattery === undefined) {
this._runningOnBattery = await this.electronService.isOnBatteryPower();
}
this._refreshing = true;
try {
const settings = await this.daemonService.getSettings();
if (this._runningOnBattery && !settings.runOnBattery) {
await this.daemonService.stopDaemon();
return;
}
else if (this._runningOnBattery && settings.batteryLevelThreshold > 0) {
const batteryLevel = await this.electronService.getBatteryLevel();
if (batteryLevel <= settings.batteryLevelThreshold) {
await this.daemonService.stopDaemon();
return;
}
this.batteryLevel = batteryLevel;
}
const syncAlreadyDisabled = this.daemonService.settings.noSync;
if (!settings.noSync && !syncAlreadyDisabled && !settings.syncOnWifi) {
@ -406,7 +436,6 @@ export class DaemonDataService {
else {
this.syncDisabledByPeriodPolicy = false;
}
}
catch(error: any) {
console.error(error);

View file

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { EventEmitter, Injectable } from '@angular/core';
// If you import a module but never use any of the imported values other than as TypeScript types,
// the resulting javascript file will look as if you never imported the module at all.
import * as childProcess from 'child_process';
@ -17,6 +17,9 @@ export class ElectronService {
private _online: boolean = false;
private _isProduction: boolean = false;
public readonly onAcPower: EventEmitter<void> = new EventEmitter<void>();
public readonly onBatteryPower: EventEmitter<void> = new EventEmitter<void>();
public get isProduction(): boolean {
return this._isProduction;
}
@ -26,6 +29,9 @@ export class ElectronService {
window.addEventListener('online', () => this._online = true);
window.addEventListener('offline', () => this._online = false);
this._isProduction = APP_CONFIG.production;
window.electronAPI.onBattery((event: any) => this.onBatteryPower.emit());
window.electronAPI.onAc((event: any) => this.onAcPower.emit());
}
public get online(): boolean {
@ -62,6 +68,19 @@ export class ElectronService {
return false;
}
public async isOnBatteryPower(): Promise<boolean> {
const promise = new Promise<boolean>((resolve) => {
window.electronAPI.onIsOnBatteryPower((event: any, onBattery: boolean) => {
window.electronAPI.unregisterOnIsOnBatteryPower();
resolve(onBattery);
});
});
window.electronAPI.isOnBatteryPower();
return await promise;
}
public async getBatteryLevel(): Promise<number> {
const promise = new Promise<number>((resolve) => {
window.electronAPI.onGetBatteryLevel((event: any, level: number) => {
@ -72,7 +91,7 @@ export class ElectronService {
window.electronAPI.getBatteryLevel();
return await promise;
return (await promise)*100;
}
public async isAutoLaunched(): Promise<boolean> {

View file

@ -186,6 +186,26 @@
</div>
</div>
<hr class="my-4">
<h4 class="mb-3">Battery</h4>
<div class="row g-3">
<div class="form-check form-switch col-md-6">
<label for="run-on-battery" class="form-check-label">Run on battery</label>
<input class="form-control form-check-input" type="checkbox" role="switch" id="run-on-battery" [checked]="currentSettings.runOnBattery" [(ngModel)]="currentSettings.runOnBattery" [ngModelOptions]="{standalone: true}">
<br>
<small class="text-body-secondary">Enable monerod to run on battery power</small>
</div>
<div class="col-md-6">
<label for="battery-threshold" class="form-label">Battery level threshold</label>
<input [disabled]="!currentSettings.runOnBattery" type="number" min="0" max="100" class="form-control" id="battery-threshold" placeholder="0" [(ngModel)]="currentSettings.batteryLevelThreshold" [ngModelOptions]="{standalone: true}">
<small class="text-body-secondary">Stop monerod when battery threshold is reached (0 to disable)</small>
</div>
</div>
<hr class="my-4">
<h4 class="mb-3">Updates</h4>

View file

@ -1,5 +1,5 @@
<div *ngIf="!daemonRunning || stoppingDaemon || restartingDaemon || enablingSync || disablingSync || installing" class="h-100 p-5 text-bg-dark rounded-3 m-4 text-center">
<h2 *ngIf="!installing && !enablingSync && !disablingSync && !daemonRunning && !startingDaemon && !stoppingDaemon && !restartingDaemon && !upgrading && daemonConfigured && !quittingDaemon"><i class="bi bi-exclamation-diamond m-4"></i> Daemon not running</h2>
<h2 *ngIf="!installing && !enablingSync && !disablingSync && !daemonRunning && !startingDaemon && !stoppingDaemon && !restartingDaemon && !upgrading && daemonConfigured && !quittingDaemon"><i class="bi bi-exclamation-diamond m-4"></i> {{ cannotRunBecauseBatteryPolicy ? batteryTooLow ? 'Battery power is too low' : 'Cannot run on battery power' : 'Daemon not running' }}</h2>
<h2 *ngIf="!installing && !enablingSync && !disablingSync && !daemonRunning && !startingDaemon && !stoppingDaemon && !restartingDaemon && !upgrading && !daemonConfigured && !quittingDaemon"><i class="bi bi-exclamation-diamond m-4"></i> Daemon not configured or installed</h2>
<h2 *ngIf="!installing && !enablingSync && !disablingSync && restartingDaemon && !upgrading"><i class="bi bi-arrow-clockwise m-4"></i> Daemon restarting</h2>
<h2 *ngIf="!installing && !enablingSync && !disablingSync && stoppingDaemon && !upgrading && !quittingDaemon"><i class="bi bi-stop-fill m-4"></i> Daemon is stopping</h2>
@ -9,7 +9,7 @@
<h2 *ngIf="!installing && enablingSync"><i class="bi bi-repeat m-4"></i>Daemon is enabling sync</h2>
<h2 *ngIf="!installing && disablingSync"><i class="bi bi-slash-circle m-4"></i>Daemon is disabling sync</h2>
<p *ngIf="!installing && !enablingSync && !disablingSync && !daemonRunning && !startingDaemon && !stoppingDaemon && !restartingDaemon && daemonConfigured && !upgrading && !quittingDaemon">Start monero daemon</p>
<p *ngIf="!installing && !enablingSync && !disablingSync && !daemonRunning && !startingDaemon && !stoppingDaemon && !restartingDaemon && daemonConfigured && !upgrading && !quittingDaemon"> {{ cannotRunBecauseBatteryPolicy ? 'Configure monero daemon' : 'Start monero daemon' }} </p>
<p *ngIf="!installing && !enablingSync && !disablingSync && !startingDaemon && !startingDaemon && !stoppingDaemon && !restartingDaemon && !daemonConfigured && !upgrading && !quittingDaemon">Configure or install monero daemon</p>
<h2 *ngIf="!installing && !enablingSync && !disablingSync && startingDaemon"><i class="bi bi-play-fill m-4"></i> Daemon is starting</h2>
@ -21,7 +21,7 @@
<p *ngIf="!installing && enablingSync">Enabling monero daemon blockchain sync</p>
<p *ngIf="!installing && disablingSync">Disabling monero daemon blokchain sync</p>
<button *ngIf="!installing && !enablingSync && !disablingSync && !startingDaemon && !stoppingDaemon && !restartingDaemon && !upgrading && daemonConfigured && !quittingDaemon" class="btn btn-outline-light" type="button" (click)="startDaemon()"><i class="bi bi-play-fill"></i> Start</button>
<button *ngIf="!cannotRunBecauseBatteryPolicy && !installing && !enablingSync && !disablingSync && !startingDaemon && !stoppingDaemon && !restartingDaemon && !upgrading && daemonConfigured && !quittingDaemon" class="btn btn-outline-light" type="button" (click)="startDaemon()"><i class="bi bi-play-fill"></i> Start</button>
<button *ngIf="!installing && !enablingSync && !disablingSync && startingDaemon" class="btn btn-outline-light" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Starting monerod
@ -53,7 +53,6 @@
Enabling sync
</button>
<button *ngIf="!installing && disablingSync" class="btn btn-outline-light" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Disabling sync

View file

@ -2,6 +2,8 @@ import { Component, OnDestroy } from '@angular/core';
import { DaemonService } from '../../../core/services/daemon/daemon.service';
import { DaemonDataService, MoneroInstallerService } from '../../../core/services';
import { Subscription } from 'rxjs';
import { DaemonSettings } from '../../../../common';
import { DaemonStatusService } from './daemon-status.service';
@Component({
selector: 'app-daemon-not-running',
@ -11,70 +13,59 @@ import { Subscription } from 'rxjs';
export class DaemonNotRunningComponent implements OnDestroy {
public get upgrading(): boolean {
return this.installer.upgrading && !this.quittingDaemon;
return this.statusService.upgrading;
}
public get installing(): boolean {
return this.installer.installing;
return this.statusService.installing;
}
public get daemonRunning(): boolean {
return this.daemonData.running && !this.startingDaemon && !this.stoppingDaemon && !this.restartingDaemon && !this.upgrading && !this.quittingDaemon;
return this.statusService.daemonRunning;
}
public daemonConfigured: boolean = true;
public get disablingSync(): boolean {
return this.daemonService.disablingSync;
return this.statusService.disablingSync;
}
public get enablingSync(): boolean {
return this.daemonService.enablingSync;
return this.statusService.enablingSync;
}
public get startingDaemon(): boolean {
return this.daemonService.starting && !this.restartingDaemon && !this.stoppingDaemon && !this.upgrading && !this.quittingDaemon;
return this.statusService.startingDaemon;
}
public get stoppingDaemon(): boolean{
return this.daemonData.stopping && !this.restartingDaemon && !this.startingDaemon && !this.upgrading && !this.quittingDaemon;
return this.statusService.stoppingDaemon;
}
public get restartingDaemon(): boolean {
return this.daemonService.restarting && ! this.upgrading && !this.quittingDaemon;
return this.statusService.restartingDaemon;
}
public get cannotRunBecauseBatteryPolicy(): boolean {
return this.statusService.cannotRunBecauseBatteryPolicy;
}
public get progressStatus(): string {
const progress = this.installer.progress;
if (progress.status == 'Downloading') {
return `${progress.status} ${progress.progress.toFixed(2)} %`;
}
return progress.status;
return this.statusService.progressStatus;
}
public get quittingDaemon(): boolean {
return this.daemonService.quitting;
return this.statusService.quittingDaemon;
}
public get batteryTooLow(): boolean {
return this.statusService.batteryTooLow;
}
private subscriptions: Subscription[] = [];
constructor(private installer: MoneroInstallerService, private daemonData: DaemonDataService, private daemonService: DaemonService) {
const onSavedSettingsSub: Subscription = this.daemonService.onSavedSettings.subscribe((settings) => {
this.daemonConfigured = settings.monerodPath != '';
});
constructor(private statusService: DaemonStatusService) {
this.daemonService.getSettings().then((settings) => {
this.daemonConfigured = settings.monerodPath != '';
}).catch((error: any) => {
console.error(error);
this.daemonConfigured = false;
});
this.daemonService.isRunning().then().catch((error: any) => console.error(error));
this.subscriptions.push(onSavedSettingsSub);
}
public ngOnDestroy(): void {
@ -83,34 +74,11 @@ export class DaemonNotRunningComponent implements OnDestroy {
}
public async startDaemon(): Promise<void> {
if (this.daemonRunning) {
console.warn("Daemon already running");
return;
}
if (this.startingDaemon || this.stoppingDaemon) {
return;
}
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
this.daemonService.startDaemon().then(() => {
resolve();
}).catch((error: any) => {
reject(new Error(`${error}`));
});
}, 500)});
await this.statusService.startDaemon();
}
public async restartDaemon(): Promise<void> {
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
this.daemonService.restartDaemon().then(() => {
resolve();
}).catch((error: any) => {
reject(new Error(`${error}`));
});
}, 500)});
await this.statusService.restartDaemon();
}
}

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DaemonStatusService } from './daemon-status.service';
describe('DaemonStatusService', () => {
let service: DaemonStatusService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DaemonStatusService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,159 @@
import { Injectable, NgZone } from '@angular/core';
import { DaemonDataService, DaemonService, ElectronService, MoneroInstallerService } from '../../../core/services';
import { Subscription } from 'rxjs';
import { DaemonSettings } from '../../../../common';
@Injectable({
providedIn: 'root'
})
export class DaemonStatusService {
public get upgrading(): boolean {
return this.installer.upgrading && !this.quittingDaemon;
}
public get installing(): boolean {
return this.installer.installing;
}
public get daemonRunning(): boolean {
return this.daemonData.running && !this.startingDaemon && !this.stoppingDaemon && !this.restartingDaemon && !this.upgrading && !this.quittingDaemon;
}
public get daemonConfigured(): boolean {
return this.settings ? this.settings.monerodPath != '' : true;
};
public get disablingSync(): boolean {
return this.daemonService.disablingSync;
}
public get enablingSync(): boolean {
return this.daemonService.enablingSync;
}
public get startingDaemon(): boolean {
return this.daemonService.starting && !this.restartingDaemon && !this.stoppingDaemon && !this.upgrading && !this.quittingDaemon;
}
public get stoppingDaemon(): boolean{
return this.daemonData.stopping && !this.restartingDaemon && !this.startingDaemon && !this.upgrading && !this.quittingDaemon;
}
public get restartingDaemon(): boolean {
return this.daemonService.restarting && ! this.upgrading && !this.quittingDaemon;
}
public get cannotRunBecauseBatteryPolicy(): boolean {
return this.settings ? (this._runningOnBattery && !this.settings.runOnBattery) || (this.settings.runOnBattery && this.settings.batteryLevelThreshold > 0 && this._batteryLevel <= this.settings.batteryLevelThreshold) : false;
}
public get progressStatus(): string {
const progress = this.installer.progress;
if (progress.status == 'Downloading') {
return `${progress.status} ${progress.progress.toFixed(2)} %`;
}
return progress.status;
}
public get quittingDaemon(): boolean {
return this.daemonService.quitting;
}
public get batteryTooLow(): boolean {
return this._batteryTooLow;
}
private subscriptions: Subscription[] = [];
private settings?: DaemonSettings;
private _runningOnBattery: boolean = false;
private _batteryTooLow: boolean = false;
private _batteryLevel: number = 0;
constructor(
private installer: MoneroInstallerService, private daemonData: DaemonDataService,
private daemonService: DaemonService, private electronService: ElectronService,
private ngZone: NgZone
) {
const onSavedSettingsSub: Subscription = this.daemonService.onSavedSettings.subscribe((settings) => {
this.refresh().then().catch((error: any) => console.error(error));
});
const onDaemonStatusSub: Subscription = this.daemonService.onDaemonStatusChanged.subscribe((running: boolean) => {
this.refresh().then().catch((error: any) => console.error(error));
});
const onAcPower: Subscription = this.electronService.onAcPower.subscribe(() => {
this.refresh().then().catch((error: any) => console.error(error));
});
const onBatteryPower: Subscription = this.electronService.onBatteryPower.subscribe(() => {
this.refresh().then().catch((error: any) => console.error(error));
});
this.refresh().then().catch((error: any) => console.error(error));
this.subscriptions.push(onSavedSettingsSub, onDaemonStatusSub, onAcPower, onBatteryPower);
}
public async refresh(): Promise<void> {
//await this.daemonService.isRunning();
this.ngZone.run(() => {
setTimeout(async () => {
this.settings = await this.daemonService.getSettings();
this._runningOnBattery = await this.electronService.isOnBatteryPower();
if (this._runningOnBattery) this._batteryLevel = await this.electronService.getBatteryLevel();
if (this.settings.runOnBattery && this._runningOnBattery && this.settings.batteryLevelThreshold > 0) {
const batteryLevel = await this.electronService.getBatteryLevel();
this._batteryTooLow = batteryLevel <= this.settings.batteryLevelThreshold;
console.log(`battery level: ${batteryLevel}, threshold: ${this.settings.batteryLevelThreshold}, too low: ${this._batteryTooLow}`);
}
else if (!this.settings.runOnBattery) {
this._batteryTooLow = false;
}
}, 0);
});
}
public Dispose(): void {
this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe());
this.subscriptions = [];
}
public async startDaemon(): Promise<void> {
if (this.daemonRunning) {
console.warn("Daemon already running");
return;
}
if (this.startingDaemon || this.stoppingDaemon) {
return;
}
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
this.daemonService.startDaemon().then(() => {
resolve();
}).catch((error: any) => {
reject(new Error(`${error}`));
});
}, 500)});
}
public async restartDaemon(): Promise<void> {
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
this.daemonService.restartDaemon().then(() => {
resolve();
}).catch((error: any) => {
reject(new Error(`${error}`));
});
}, 500)});
}
}

View file

@ -44,7 +44,7 @@
<ul class="navbar-nav flex-row">
<li *ngIf="!quitting && !running && !stopping && !restarting && !installing && !upgrading && daemonConfigured" class="nav-item text-nowrap">
<button [disabled]="starting || enablingSync || disablingSync" class="btn btn-outline-secondary px-3 text-white" type="button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Start Daemon" aria-expanded="false" aria-label="Start daemon" (click)="startDaemon()">
<button [disabled]="starting || enablingSync || disablingSync || cannotStart" class="btn btn-outline-secondary px-3 text-white" type="button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Start Daemon" aria-expanded="false" aria-label="Start daemon" (click)="startDaemon()">
<i class="bi bi-play-fill"></i>
</button>
&nbsp;

View file

@ -6,6 +6,7 @@ import { DaemonDataService, MoneroInstallerService } from '../../../core/service
import { DaemonSettings } from '../../../../common';
import { Subscription } from 'rxjs';
import { Tooltip } from 'bootstrap';
import { DaemonStatusService } from '../daemon-not-running/daemon-status.service';
@Component({
selector: 'app-navbar',
@ -70,7 +71,11 @@ export class NavbarComponent implements AfterViewInit, OnDestroy {
private daemonSettings: DaemonSettings = new DaemonSettings();
private subscriptions: Subscription[] = [];
constructor(private navbarService: NavbarService, private daemonService: DaemonService, private daemonData: DaemonDataService, private installerService: MoneroInstallerService, private ngZone: NgZone) {
public get cannotStart(): boolean {
return this.statusService.cannotRunBecauseBatteryPolicy;
}
constructor(private navbarService: NavbarService, private daemonService: DaemonService, private daemonData: DaemonDataService, private installerService: MoneroInstallerService, private statusService: DaemonStatusService, private ngZone: NgZone) {
const onSavedSettingsSub: Subscription = this.daemonService.onSavedSettings.subscribe((settings: DaemonSettings) => {
this.daemonSettings = settings;
});

View file

@ -9,6 +9,9 @@ export class DaemonSettings {
public syncPeriodFrom: string = '00:00';
public syncPeriodTo: string = '00:00';
public runOnBattery: boolean = true;
public batteryLevelThreshold: number = 0;
public upgradeAutomatically: boolean = false;
public downloadUpgradePath: string = '';

View file

@ -155,6 +155,12 @@ declare global {
getBatteryLevel: () => void;
onGetBatteryLevel: (callback: (event: any, level: number) => void) => void;
unregisterOnGetBatteryLevel: () => void;
isOnBatteryPower: () => void;
onIsOnBatteryPower: (callback: (event: any, onBattery: boolean) => void) => void;
unregisterOnIsOnBatteryPower: () => void;
onBattery: (callback: (event: any) => void) => void;
onAc: (callback: (event: any) => void) => void;
};
}
}