UNPKG

@minimaltech/electron-infra

Version:

Minimal Technology ElectronJS Infrastructure

438 lines 20.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseElectronApplication = exports.AbstractElectronApplication = void 0; const constants_1 = require("../../common/constants"); const keys_1 = require("../../common/keys"); const node_infra_1 = require("@minimaltech/node-infra"); const core_1 = require("@minimaltech/node-infra/@loopback/core"); const electron_1 = require("electron"); const fs_1 = __importDefault(require("fs")); const node_child_process_1 = require("node:child_process"); const node_os_1 = __importDefault(require("node:os")); const path_1 = __importDefault(require("path")); const services_1 = require("../services"); const self_code_siging_helper_1 = require("../../helpers/self-code-siging.helper"); // -------------------------------------------------------------------------------- class AbstractElectronApplication extends core_1.Application { // routes: Map<string | symbol, Function>; // ------------------------------------------------------------------------------ constructor(opts) { var _a, _b; super(); this.logger = node_infra_1.LoggerFactory.getLogger([opts.scope]); // this.routes = new Map<string | symbol, Function>(); this.application = opts.application; if (opts.appName) { this.application.setName(opts.appName); } this.windowManager = (_a = opts.windowManager) !== null && _a !== void 0 ? _a : services_1.WindowManager.getInstance(); this.autoUpdaterOptions = opts.autoUpdaterOptions; if (!((_b = this.autoUpdaterOptions) === null || _b === void 0 ? void 0 : _b.use)) { return; } if (!opts.autoUpdater) { throw (0, node_infra_1.getError)({ message: 'Missing arg opts.autoUpdater!', }); } this.autoUpdater = opts.autoUpdater; } getAutoUpdater() { return this.autoUpdater; } executeWin32SignatureVerification() { var _a; if (!((_a = this.autoUpdaterOptions) === null || _a === void 0 ? void 0 : _a.use)) { return; } const verifyOptions = this.autoUpdaterOptions.verify; if (verifyOptions.caType !== constants_1.CASignTypes.SELF_SIGNED_CA) { return; } const { verifySignature = self_code_siging_helper_1.verifySelfCodeSigningSignature } = verifyOptions; const nsisUpdater = this.autoUpdater; nsisUpdater.verifyUpdateCodeSignature = (publishers, unescapedTempUpdateFile) => __awaiter(this, void 0, void 0, function* () { if (!this.autoUpdaterOptions) { return 'Invalid verifySelfCodeSigningSignature implementation!'; } try { const tmpPath = path_1.default.normalize(unescapedTempUpdateFile.replace(/'/g, "''")); this.logger.info('[verifyUpdateCodeSignature] Verifying signature | publisherNames: %s | tmpPath: %s', publishers, tmpPath); const signatureAuthRs = (0, node_child_process_1.execFileSync)(`set "PSModulePath=" & chcp 65001 >NUL & powershell.exe`, [ '-NoProfile', '-NonInteractive', '-InputFormat', 'None', '-Command', `"Get-AuthenticodeSignature -LiteralPath '${tmpPath}' | ConvertTo-Json -Compress"`, ], { shell: true, timeout: 20000 }); const verifyRs = yield verifySignature({ publishers, tmpPath, signature: JSON.parse(signatureAuthRs.toString('utf8')), autoUpdaterOptions: this.autoUpdaterOptions, }); this.logger.info('[verifyUpdateCodeSignature] verifyRs: %j', verifyRs); return verifyRs; } catch (error) { const message = '[verifyUpdateCodeSignature] Failed to get authentication code signature!'; this.logger.error('%s | Error: %s', message, error); return Promise.resolve(message); } }); } bindAutoUpdater() { var _a; if (!((_a = this.autoUpdaterOptions) === null || _a === void 0 ? void 0 : _a.use)) { this.logger.warn('[bindAutoUpdater] Ignore configuring Application Auto Updater!'); return; } this.autoUpdater.autoDownload = true; this.autoUpdater.autoInstallOnAppQuit = true; this.autoUpdater.autoRunAppAfterInstall = true; this.autoUpdater.on('error', error => { return this.onUpdateError(error); }); this.autoUpdater.on('checking-for-update', () => { return this.onCheckingForUpdate(); }); this.autoUpdater.on('update-available', updateInfo => { this.onUpdateAvailable(updateInfo); }); this.autoUpdater.on('update-not-available', updateInfo => { this.onUpdateNotAvailable(updateInfo); }); this.autoUpdater.on('download-progress', progress => { this.onDownloadProgress(progress); }); this.autoUpdater.on('update-downloaded', updateInfo => { this.onUpdateDownloaded(updateInfo); }); const platform = node_os_1.default.platform(); switch (platform) { case 'win32': { this.executeWin32SignatureVerification(); break; } default: { this.logger.warn('[bindAutoUpdater] Unsupported custom verifyUpdateCodeSignature | platform: %s | supported: %s', platform, ['win32']); return; } } } // ------------------------------------------------------------------------------ getApplicationInstance() { return this.application; } getDialog() { return electron_1.dialog; } getWindowManager() { return this.windowManager; } getPreloadPath(opts) { const { fileName = 'preload.js' } = opts !== null && opts !== void 0 ? opts : { fileName: 'preload.js' }; const projectRoot = this.getProjectRoot(); const preloadPath = path_1.default.join(projectRoot, fileName); this.logger.debug('[getPreloadPath] preloadPath: %s', preloadPath); if (!fs_1.default.existsSync(preloadPath)) { throw (0, node_infra_1.getError)({ statusCode: node_infra_1.ResultCodes.RS_4.NotFound, message: `[getPreloadPath] Path: ${preloadPath} | preloadPath NOT FOUND`, }); } return preloadPath; } // ------------------------------------------------------------------------------ // Context Binding // ------------------------------------------------------------------------------ preConfigure() { const isFirstInstance = this.application.requestSingleInstanceLock(); if (!isFirstInstance) { this.logger.warn('[preConfigure] Quiting application | Application instance was locked by another process | Please check whether or not another application was opening!'); this.application.quit(); } this.bindAutoUpdater(); this.bindContext(); this.bindEvents(); } // ------------------------------------------------------------------------------ postConfigure() { this.buildRoutes(); } // ------------------------------------------------------------------------------ getMethodExecutor(opts) { var _a; const { binding, methodName } = opts; const ctor = binding.valueConstructor; const ctorPrototype = ctor === null || ctor === void 0 ? void 0 : ctor.prototype; const className = (_a = ctorPrototype === null || ctorPrototype === void 0 ? void 0 : ctorPrototype.name) !== null && _a !== void 0 ? _a : ''; return (...args) => { if ((args === null || args === void 0 ? void 0 : args.length) > 1) { this.logger.warn('[%s] executor use only FIRST arg | Please check again caller args!', methodName); } const controller = binding.getValue(this); if (!(controller === null || controller === void 0 ? void 0 : controller[methodName])) { throw (0, node_infra_1.getError)({ message: `[getMethodExecutor] Class: ${className} | Method: ${methodName} | Method is not available!`, }); } return Reflect.apply(controller[methodName], controller, args); }; } // ------------------------------------------------------------------------------ buildRoute(opts) { const { binding, methodName } = opts; const ctor = binding.valueConstructor; const ctorPrototype = ctor === null || ctor === void 0 ? void 0 : ctor.prototype; if (!ctorPrototype) { throw (0, node_infra_1.getError)({ message: `[buildRoute] Binding: ${binding.key} | Class: ${ctor === null || ctor === void 0 ? void 0 : ctor.name} | method: ${methodName} | Skip build route | Invalid ctor prototype`, }); } const exposeMetadata = core_1.MetadataInspector.getMethodMetadata(keys_1.BindingKeys.EXPOSE_METHOD_KEY, ctorPrototype, methodName); if (!exposeMetadata) { this.logger.warn('[buildRoute] Class: %s | Method: %s | Skip method initialize!', ctor.name, methodName); return; } const { verb } = exposeMetadata; let ipcAction = null; const executor = this.getMethodExecutor(opts); const ipcChannel = `${ctor.name}.${methodName}`; switch (verb) { case constants_1.ExposeVerbs.SUBSCRIBER: { ipcAction = 'on'; electron_1.ipcMain.on(ipcChannel, (_event, ...args) => { executor(...args); }); break; } case constants_1.ExposeVerbs.HANDLER: { ipcAction = 'handle'; electron_1.ipcMain.handle(ipcChannel, (_event, ...args) => { return Promise.resolve(executor(...args)); }); break; } default: { break; } } this.logger.info('[buildRoute][%s] Binding route | Class: %s | Method: %s | ipcMethod: %s', ipcAction, ctor.name, methodName, ipcChannel); /* this.routes.set(`${ctor.name}_${methodName}`, () => { executor!(method ?? methodName, (_event, ...args: any[]) => {}); }); */ } // ------------------------------------------------------------------------------ buildRoutes() { var _a; const controllers = this.findByTag('controllers'); for (const binding of controllers) { const prototype = (_a = binding.valueConstructor) === null || _a === void 0 ? void 0 : _a.prototype; if (!prototype) { continue; } const methodDescriptors = Object.getOwnPropertyDescriptors(prototype); for (const methodName in methodDescriptors) { if (methodName === 'constructor') { continue; } this.buildRoute({ binding, methodName }); /* this.routes.set(methodName, (...args: any[]) => { const controller = binding.getValue(this); Reflect.apply(controller[methodName], controller, args); }); */ } } } // ------------------------------------------------------------------------------ injectable(scope, value, tags) { this.bind(`${scope}.${value.name}`) .toInjectable(value) .tag(...(tags !== null && tags !== void 0 ? tags : []), scope); } // ------------------------------------------------------------------------------ datasource(value) { this.injectable('datasources', value); } // ------------------------------------------------------------------------------ repository(value) { this.injectable('repositories', value); } // ------------------------------------------------------------------------------ bindEvents() { if (!this.application) { throw (0, node_infra_1.getError)({ message: '[binding] Invalid application instance to bind', }); } this.on('before-migrate', () => { Promise.resolve(this.onBeforeMigrate()) .then(() => { this.emit('migrate'); }) .catch(error => { this.logger.error('[onBeforeMigrate] Error while handling before migrate application | Error: %s', error); }); }); this.on('migrate', () => { Promise.resolve(this.onMigrate()) .then(() => { this.emit('after-migrate'); }) .catch(error => { this.logger.error('[migrate] Error while handling migrate application | Error: %s', error); }); }); this.on('after-migrate', () => { Promise.resolve(this.onAfterMigrate()) .then(() => { this.onReady(); }) .catch(error => { this.logger.error('[onAfterMigrate] Error while handling after migrate application | Error: %s', error); }); }); this.application.on('will-finish-launching', () => this.onWillFinishLaunching()); this.application.on('ready', (_event, _launchInfo) => { this.emit('before-migrate'); }); this.application.on('second-instance', (event, args, dir, additionalData) => this.onSecondApplicationInstance(event, args, dir, additionalData)); this.application.on('browser-window-created', (event, window) => this.onBrowserWindowCreated(event, window)); this.application.on('window-all-closed', () => this.onAllWindowsClosed()); this.application.on('will-quit', event => this.onWillQuit(event)); this.application.on('before-quit', event => this.onBeforeQuit(event)); this.application.on('quit', (event, exitCode) => this.onQuit(event, exitCode)); } // ------------------------------------------------------------------------------ start() { const _super = Object.create(null, { start: { get: () => super.start } }); return __awaiter(this, void 0, void 0, function* () { _super.start.call(this); yield this.preConfigure(); yield this.postConfigure(); }); } } exports.AbstractElectronApplication = AbstractElectronApplication; // -------------------------------------------------------------------------------- class BaseElectronApplication extends AbstractElectronApplication { // ---------------------------------------------------------------------- // Main Application Events // ---------------------------------------------------------------------- onBeforeMigrate() { return; } onMigrate() { return; } onAfterMigrate() { return; } onWillFinishLaunching() { this.logger.debug('[onWillFinishLaunching] Application finishing launching'); } onSecondApplicationInstance(event, args, _dir, _additionalData) { this.logger.debug('[onSecondApplicationInstance] New Application was requested to create | Args: %j', event, args); } onBrowserWindowCreated(event, window) { this.logger.debug('[onBrowserWindowCreated] New BrowserWindow CREATED | Window: %j', event, window); } onAllWindowsClosed() { this.logger.debug('[onAllWindowsClosed] All BrowserWindows was CLOSED | Quiting application!'); if (process.platform === 'darwin') { return; } this.application.quit(); } onWillQuit(event) { this.logger.debug('[onWillQuit] Application WILL_QUIT', event); } onBeforeQuit(event) { this.logger.debug('[onBeforeQuit] Application BEFORE_QUIT', event); } onQuit(event, exitCode) { this.logger.debug('[onQuit] Application QUIT | exitCode: %s', event, exitCode); } // ---------------------------------------------------------------------- // Application Updater Events // ---------------------------------------------------------------------- onUpdateError(error) { this.logger.error('[onUpdateError] Failed to update new version | Error: %s', error); this.getDialog().showMessageBoxSync({ type: 'error', title: 'Update Error', message: `Failed to update new version\n\n${error.name}\n${error.message}`, }); this.application.quit(); } onCheckingForUpdate() { this.logger.info('[onCheckingForUpdate] Checking for update...'); } onUpdateNotAvailable(updateInfo) { const currentVersion = this.application.getVersion(); this.logger.info('[onUpdateNotAvailable] currentVersion: %s | updateInfo: %j', currentVersion, updateInfo); } onUpdateAvailable(updateInfo) { return __awaiter(this, void 0, void 0, function* () { var _a; if (!((_a = this.autoUpdaterOptions) === null || _a === void 0 ? void 0 : _a.use)) { return; } const { forceUpdateNewVersion } = this.autoUpdaterOptions; const currentVersion = this.application.getVersion(); this.logger.info('[onUpdateAvailable] New version is now available | currentVersion: %s | updateInfo: %j', currentVersion, updateInfo); // Force update new application version if (forceUpdateNewVersion) { this.logger.info('[onUpdateAvailable] FORCE update new version | Start downloading new version...!'); yield this.autoUpdater.downloadUpdate(); return; } // Notify new application version const applicationName = this.application.getName(); const userChoice = this.getDialog().showMessageBoxSync({ type: 'info', title: 'Update Available', message: `New ${applicationName} version is now available\n\nCurrent version: ${currentVersion}\nNew version: ${updateInfo.version}\nRelease date: ${updateInfo.releaseDate}`, buttons: ['NO', 'YES'], }); if (!userChoice) { this.logger.error('[onUpdateAvailable] DENIED update new version | Force close application!'); this.application.quit(); } this.logger.info('[onUpdateAvailable] ACCEPTED update new version | Start downloading new version...!'); yield this.autoUpdater.downloadUpdate(); }); } onDownloadProgress(progress) { this.logger.info('[onDownloadProgress] Downloading new version | progress: %j', progress); } onUpdateDownloaded(updateInfo) { const willQuitAndInstall = this.autoUpdaterOptions && this.autoUpdaterOptions.use && this.autoUpdaterOptions.autoInstallAfterDownloaded; this.logger.info('[onUpdateDownloaded] willQuitAndInstall: %s | updateInfo: %j', willQuitAndInstall, updateInfo); if (!willQuitAndInstall) { return; } this.logger.info('[onUpdateDownloaded] Quit and install new version | updateInfo: %j', updateInfo); return this.autoUpdater.quitAndInstall(); } } exports.BaseElectronApplication = BaseElectronApplication; //# sourceMappingURL=base.application.js.map