@minimaltech/electron-infra
Version:
Minimal Technology ElectronJS Infrastructure
438 lines • 20.6 kB
JavaScript
;
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