From c95017f4d9ae457b9bbd3a2af80f2c61f97aa107 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Mon, 14 Oct 2024 23:08:02 +0200 Subject: [PATCH] Automatic upgrade and disable sync on wifi implementation --- README.md | 1 + app/main.ts | 99 +++++++- app/preload.js | 3 + package.json | 1 + .../services/daemon/daemon-data.service.ts | 51 ++++- .../core/services/daemon/daemon.service.ts | 215 +++++++++++++----- src/app/pages/detail/detail.component.ts | 30 ++- src/app/pages/version/version.component.ts | 24 +- src/common/DaemonSettings.ts | 2 +- src/polyfills.ts | 6 +- 10 files changed, 336 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index effcff9..dd6142c 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/app/main.ts b/app/main.ts index 776a6ae..4b876f6 100644 --- a/app/main.ts +++ b/app/main.ts @@ -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 { + 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(); @@ -168,6 +237,8 @@ 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); @@ -175,19 +246,32 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr // 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) => { diff --git a/app/preload.js b/app/preload.js index 5bbe657..c32e728 100644 --- a/app/preload.js +++ b/app/preload.js @@ -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); }, diff --git a/package.json b/package.json index bbfebe4..097cb2c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/core/services/daemon/daemon-data.service.ts b/src/app/core/services/daemon/daemon-data.service.ts index d18570b..f130388 100644 --- a/src/app/core/services/daemon/daemon-data.service.ts +++ b/src/app/core/services/daemon/daemon-data.service.ts @@ -60,7 +60,7 @@ export class DaemonDataService { private _txPoolBacklog: TxBacklogEntry[] = []; private _gettingTxPoolBackLog: boolean = false; - public readonly syncStart: EventEmitter = new EventEmitter(); + public readonly syncStart: EventEmitter<{ first: boolean }> = new EventEmitter<{ first: boolean }>(); public readonly syncEnd: EventEmitter = new EventEmitter(); public readonly syncError: EventEmitter = new EventEmitter(); @@ -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.refreshInterval = setInterval(() => { - this.refresh().then().catch((error: any) => { - console.error(error); - }); - },this.refreshTimeoutMs); + + 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(); diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index e5f0740..3070da2 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -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 = new EventEmitter(); public readonly onDaemonStopStart: EventEmitter = new EventEmitter(); public readonly onDaemonStopEnd: EventEmitter = new EventEmitter(); @@ -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 { + try { + return new Promise((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 { + 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 { + 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 { 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 { @@ -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 { @@ -281,36 +336,65 @@ export class DaemonService { } } - public async startDaemon(): Promise { - if (await this.isRunning()) { - console.warn("Daemon already running"); - return; - } + public async startDaemon(customSettings?: DaemonSettings): Promise { + await new Promise(async (resolve, reject) => { + if (await this.isRunning()) { + console.warn("Daemon already running"); + return; + } + + this.starting = true; + + console.log("Starting daemon"); - this.starting = true; + this.settings = customSettings ? customSettings : await this.getSettings(); + + if (!this.settings.noSync && !this.settings.syncOnWifi) { + const wifiConnected = await this.isWifiConnected(); - 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()); - } + if (wifiConnected) { + console.log("Disabling sync ..."); + + this.settings.noSync = true; + } + } + else if (!this.settings.noSync && !this.settings.syncOnWifi) { + const wifiConnected = await this.isWifiConnected(); - await this.delay(3000); + if (!wifiConnected) { + console.log("Enabling sync ..."); + + this.settings.noSync = false; + } + } - if (await this.isRunning(true)) { - console.log("Daemon started"); - this.onDaemonStatusChanged.emit(true); - } - else - { - console.log("Daemon not started"); - this.onDaemonStatusChanged.emit(false); - } + window.electronAPI.onMonerodStarted((event: any, started: boolean) => { + console.debug(event); + + if (started) { + console.log("Daemon started"); + this.onDaemonStatusChanged.emit(true); + resolve(); + } + 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 { @@ -965,6 +1049,25 @@ export class DaemonService { return response.height; } + public async upgrade(): Promise { + 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 { const response = await this.callRpc(new UpdateRequest(command, path)); diff --git a/src/app/pages/detail/detail.component.ts b/src/app/pages/detail/detail.component.ts index a70f75d..3ce0ac7 100644 --- a/src/app/pages/detail/detail.component.ts +++ b/src/app/pages/detail/detail.component.ts @@ -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), diff --git a/src/app/pages/version/version.component.ts b/src/app/pages/version/version.component.ts index 48bb1b4..5cfb0e4 100644 --- a/src/app/pages/version/version.component.ts +++ b/src/app/pages/version/version.component.ts @@ -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 { + 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; diff --git a/src/common/DaemonSettings.ts b/src/common/DaemonSettings.ts index 2704b96..65cb69d 100644 --- a/src/common/DaemonSettings.ts +++ b/src/common/DaemonSettings.ts @@ -184,7 +184,7 @@ export class DaemonSettings { public toCommandOptions(): string[] { const options: string[] = []; if (this.monerodPath != '') options.push(this.monerodPath); - + if (this.mainnet) options.push(`--mainnet`); else if (this.testnet) options.push(`--testnet`); else if (this.stagenet) options.push(`--stagenet`); diff --git a/src/polyfills.ts b/src/polyfills.ts index 8708fe4..4b21307 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -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; + }; } } \ No newline at end of file