feat(desktop): add logger and module loading refactor

This commit is contained in:
Rafael Keramidas
2021-01-15 20:14:32 +01:00
parent 1936ea70da
commit dd20fbca8a
35 changed files with 977 additions and 170 deletions

102
docs/misc/desktop_logger.md Normal file
View File

@@ -0,0 +1,102 @@
# Desktop Logger
The desktop application includes a logging library to display various types of log in the console or in a file.
Four (or five if we count 'mute') log levels are currently implemented:
- error (1)
- warn (2)
- info (3)
- debug (4)
All messages with an inferior level to the selected one will be displayed. For example, if the selected log level is _info_, then it will also display _warn_ and _error_ messages.
## How to enable logging
Logging can be enabled by running Suite with the command line flag `--log-level=LEVEL` (replace _LEVEL_ with _error_, _warn_, _info_ or _debug_ based on the logging you wish to display). Additional command line flags can be found on the [Suite-Desktop page](../packages/suite-desktop.md).
## API
### Exported Types
#### LogLevel
Any of the following values:
- mute (0)
- error (1)
- warn (2)
- info (3)
- debug (4)
#### Options
| name | type | default value | description |
| --- | --- | --- | --- |
| colors | boolean | `true` | Console output has colors |
| writeToConsole | boolean | `true` | Output is displayed in the console |
| writeToDisk | boolean | `false` | Output is written to a file |
| outputFile | string | `'log-%ts.txt'` | file name for the output |
| outputPath | string | Electron app log path or CWD | path for the output |
| logFormat | string | `'%dt - %lvl(%top): %msg'` | Output format of the log |
### String formatters
The options `outputFile` and `logFormat` can be used with some expressions, prefixed with the percent (%) symbol, to apply certain dynamic elements. It is for example possible to display a timestamp or the current date & time in the listed options above. While some expressions can be used in any strings, some strings have their own expressions.
#### Global
| Expression | Example output | Description |
| --- | --- | --- |
| `%ts` | `1611054460306` | Timestamp |
| `%dt` | `2021-01-19T11:08:22.244Z` | Date and time in ISO format (ISO 8601) |
#### logFormat
| Expression | Example output | Description |
| --- | --- | --- |
| `%lvl` | `INFO` | Level in letters and upper case |
| `%top` | `Example` | Topic |
| `%msg` | `Example message` | Message |
### Constructor
The constructor has the following parameters:
- level (LogLevel): Selected log level (see LogLevels in **Exported Types** above)
- options (Options): Optional parameter containing settings for the logger (see Options in **Exported Types** above)
### Log methods
All log methods have the same signature as they are just wrappers around a private logging method.
The following methods are available:
- `error(topic: string, messages: string | string[]); // level: 1`
- `warn(topic: string, messages: string | string[]); // level: 2`
- `info(topic: string, messages: string | string[]); // level: 3`
- `debug(topic: string, messages: string | string[]); // level: 4`
Parameters:
- **topic** (string): Message topic
- **messages** (string | string[]): Single message or array of messages which will be displayed one by line.
### Exit method
The `exit()` method is used to close the write stream of the log file. If you are not planning to write logs to disk, you won't need to use this method. Otherwise it is highly advised to place this inside exit/crash callbacks.
### Example
```typeScript
const logger = new Logger('warn', {
colors: false, // Turning off colors
logFormat: '%lvl: %msg', // Level and message only
writeToDisk: true, // Write to disk
outputFile: 'log-desktop.txt', // Static file name, will be overwritten if it exists
});
// These messages will be printed with the level provided above
logger.error('example', 'This is an example error message with the topic "example"');
logger.warn('example', 'This is an example warn message with the topic "example"');
// And these won't
logger.info('example', 'This is an example info message with the topic "example"');
logger.debug('example', 'This is an example debug message with the topic "example"');
// Closes the write stream (only needed of writing the log file)
logger.exit();
```
## Q&A
### How to format a string?
The library does not have any formatting capabilities as JavaScript already has templating features built-in. Simply use string literals (for example: ```logger.info('Topic', `My string ${myvar}.`)```).
### How to output an object?
The library does not include any helper for this as there is already a language feature that does this. Simply use `JSON.stringify(myObject)`.
### How can I write the log in JSON format?
You can change the `logOutput` option to be formatted like JSON. For example: `logOutput: '{ "ts": "%ts", "lvl": "%lvl", "topic": "%top", "message": "%msg" },'`.
In order to display/use it properly, you will have to edit the output a little bit. Wrap all the messages in square brakets ([, ]) and remove the comma (,) from the last message.

View File

@@ -5,6 +5,9 @@ Using VS Code configuration files (inside `.vscode`), Suite Desktop can be built
Known issue: The devtools might blank out at launch. If this happens, simply close and re-open the devtools (CTRL + SHIFT + I).
## Logging
_TODO_
## Shortcuts
_TODO_
@@ -18,3 +21,7 @@ Available flags:
| `--disable-csp` | Disables the Content Security Policy. Necessary for using DevTools in a production build. |
| `--pre-release` | Tells the auto-updater to fetch pre-release updates. |
| `--bridge-dev` | Instruct Bridge to support emulator (starts Bridge with `-e 21324`). |
| `--log-level=NAME` | Set the logging level. Available levels are [name (value)]: error (1), warn (2), info(3), debug (4). All logs with a value equal or lower to the selected log level will be displayed. |
| `--log-write` | Write log to disk |
| `--log-no-print` | Disable the log priting in the console. |
| `--log-output=FILENAME` | Name of the output file (default: log-%ts.txt) |

View File

@@ -143,11 +143,13 @@
"afterSign": "scripts/notarize.js"
},
"dependencies": {
"chalk": "^4.1.0",
"electron-is-dev": "^1.2.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.5",
"node-fetch": "^2.6.1"
"node-fetch": "^2.6.1",
"systeminformation": "^4.34.7"
},
"devDependencies": {
"@sentry/browser": "^5.29.2",

View File

@@ -1,14 +1,21 @@
const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const glob = require('glob');
const { build } = require('esbuild');
const pkg = require('../package.json');
const electronSource = path.join(__dirname, '..', 'src-electron');
const isDev = process.env.NODE_ENV !== 'production';
const gitRevision = child_process.execSync('git rev-parse HEAD').toString().trim();
const modulePath = path.join(electronSource, 'modules');
const modules = glob.sync(`${modulePath}/**/*.ts`).map(m => `modules/${m.replace(modulePath, '')}`);
console.log('[Electron Build] Starting...');
const hrstart = process.hrtime();
build({
entryPoints: ['app', 'preload'].map(f => path.join(electronSource, `${f}.ts`)),
entryPoints: ['app.ts', 'preload.ts', ...modules].map(f => path.join(electronSource, f)),
platform: 'node',
bundle: true,
target: 'node12.18.2', // Electron 11
@@ -20,6 +27,9 @@ build({
sourcemap: isDev,
minify: !isDev,
outdir: path.join(__dirname, '..', 'dist'),
define: {
'process.env.COMMITHASH': JSON.stringify(gitRevision),
},
})
.then(() => {
const hrend = process.hrtime(hrstart);

View File

@@ -14,8 +14,7 @@ next.prepare().then(() => {
const requestHandler = next.getRequestHandler();
const server = createServer(requestHandler).listen(8000, () => {
if (!launchElectron) {
console.error('process.env.LAUNCH_ELECTRON not set to true')
return process.exit(1);
return;
}
const electron = exec('yarn run dev:run', {

View File

@@ -0,0 +1,92 @@
import fs from 'fs';
import path from 'path';
import Logger, { Options } from '../libs/logger';
const testOptions: Options = {
colors: false,
logFormat: '%lvl(%top): %msg',
outputFile: 'test-log.txt',
outputPath: __dirname,
};
const output = path.join(testOptions.outputPath, testOptions.outputFile);
describe('logger', () => {
let spy: any;
beforeEach(() => {
spy = jest.spyOn(global.console, 'log').mockImplementation();
});
afterEach(() => {
spy.mockRestore();
});
afterAll(() => {
// Clean up (delete log file)
fs.unlinkSync(output);
});
it('Level "mute" should not log any messages', () => {
const logger = new Logger('mute', testOptions);
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
expect(spy).toHaveBeenCalledTimes(0);
});
it('Level "error" should only show error messages', () => {
const logger = new Logger('error', testOptions);
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
expect(console.log).toHaveBeenCalledTimes(1);
expect(spy.mock.calls).toEqual([['ERROR(test): A']]);
});
it('Level "warn" should show error and warning messages', () => {
const logger = new Logger('warn', testOptions);
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
expect(console.log).toHaveBeenCalledTimes(2);
expect(spy.mock.calls).toEqual([['ERROR(test): A'], ['WARN(test): B']]);
});
it('Level "info" should show all messages except debug', () => {
const logger = new Logger('info', testOptions);
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
expect(console.log).toHaveBeenCalledTimes(3);
expect(spy.mock.calls).toEqual([['ERROR(test): A'], ['WARN(test): B'], ['INFO(test): C']]);
});
it('Level "debug" should not log all messages', () => {
const logger = new Logger('debug', testOptions);
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
expect(console.log).toHaveBeenCalledTimes(4);
expect(spy.mock.calls).toEqual([
['ERROR(test): A'],
['WARN(test): B'],
['INFO(test): C'],
['DEBUG(test): D'],
]);
});
it('Output file should be written', async () => {
const logger = new Logger('debug', {
...testOptions,
writeToDisk: true,
});
logger.error('test', 'A');
logger.warn('test', 'B');
logger.info('test', 'C');
logger.debug('test', 'D');
logger.exit();
// Delay for file to finish writing
await new Promise(res => setTimeout(res, 1000));
const logFile = fs.readFileSync(output, { encoding: 'utf8' });
expect(logFile).toBe('ERROR(test): A\nWARN(test): B\nINFO(test): C\nDEBUG(test): D\n');
});
});

View File

@@ -3,25 +3,12 @@ import url from 'url';
import { app, BrowserWindow, ipcMain } from 'electron';
import isDev from 'electron-is-dev';
// Libs
import { RESOURCES, PROTOCOL } from '@lib/constants';
import { PROTOCOL } from '@lib/constants';
import * as store from '@lib/store';
import { MIN_HEIGHT, MIN_WIDTH } from '@lib/screen';
// Modules
import csp from '@module/csp';
import autoUpdater from '@module/auto-updater';
import windowControls from '@module/window-controls';
import tor from '@module/tor';
import bridge from '@module/bridge';
import metadata from '@module/metadata';
import menu from '@module/menu';
import shortcuts from '@module/shortcuts';
import devTools from '@module/dev-tools';
import httpReceiver from '@module/http-receiver';
import requestFilter from '@module/request-filter';
import externalLinks from '@module/external-links';
import fileProtocol from '@module/file-protocol';
import Logger, { LogLevel } from '@lib/logger';
import { buildInfo, computerInfo } from '@lib/info';
import modules from '@lib/modules';
let mainWindow: BrowserWindow;
const APP_NAME = 'Trezor Suite';
@@ -33,11 +20,32 @@ const src = isDev
slashes: true,
});
// Logger
const log = {
level: app.commandLine.getSwitchValue('log-level') || (isDev ? 'debug' : 'error'),
writeToConsole: !app.commandLine.hasSwitch('log-no-print'),
writeToDisk: app.commandLine.hasSwitch('log-write'),
outputFile: app.commandLine.getSwitchValue('log-output'),
};
const logger = new Logger(log.level as LogLevel, { ...log });
// Globals
global.logger = logger;
global.resourcesPath = isDev
? path.join(__dirname, '..', 'public', 'static')
: process.resourcesPath;
global.quitOnWindowClose = false;
logger.info('main', 'Application starting');
const init = async () => {
buildInfo();
await computerInfo();
const winBounds = store.getWinBounds();
logger.debug('init', `Create Browswer Window (${winBounds.width}x${winBounds.height})`);
mainWindow = new BrowserWindow({
title: APP_NAME,
width: winBounds.width,
@@ -54,32 +62,19 @@ const init = async () => {
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
icon: path.join(RESOURCES, 'images', 'icons', '512x512.png'),
icon: path.join(global.resourcesPath, 'images', 'icons', '512x512.png'),
});
// Load page
logger.debug('init', `Load URL (${src})`);
mainWindow.loadURL(src);
// Modules
menu(mainWindow);
shortcuts(mainWindow, src);
requestFilter(mainWindow);
externalLinks(mainWindow, store);
windowControls(mainWindow, store);
httpReceiver(mainWindow, src);
metadata();
await bridge();
await tor(mainWindow, store);
if (isDev) {
// Dev only modules
devTools(mainWindow);
} else {
// Prod only modules
csp(mainWindow);
fileProtocol(mainWindow, src);
autoUpdater(mainWindow, store);
}
await modules({
mainWindow,
src,
store,
});
};
app.name = APP_NAME; // overrides @trezor/suite-desktop app name in menu
@@ -88,9 +83,11 @@ app.on('ready', init);
app.on('before-quit', () => {
if (!mainWindow) return;
mainWindow.removeAllListeners();
logger.exit();
});
ipcMain.on('app/restart', () => {
logger.info('main', 'App restart requested');
app.relaunch();
app.exit();
});

View File

@@ -1,10 +1,54 @@
// Globals
declare namespace NodeJS {
export interface Global {
logger: ILogger;
resourcesPath: string;
quitOnWindowClose: boolean;
}
}
declare interface ILogger {
/**
* Exit the Logger (will correctly end the log file)
*/
exit();
/**
* Error message (level: 1)
* @param topic(string) - Log topic
* @param message(string | string[]) - Message content(s)
*/
error(topic: string, message: string | string[]);
/**
* Warning message (level: 2)
* @param topic(string) - Log topic
* @param message(string | string[]) - Message content(s)
*/
warn(topic: string, message: string | string[]);
/**
* Info message (level: 3)
* @param topic(string) - Log topic
* @param message(string | string[]) - Message content(s)
*/
info(topic: string, message: string | string[]);
/**
* Debug message (level: 4)
* @param topic(string) - Log topic
* @param message(string | string[]) - Message content(s)
*/
debug(topic: string, message: string | string[]);
/**
* Log Level getter
*/
level: LogLevel;
}
// Dependencies
declare type Dependencies = {
mainWindow?: Electron.BrowserWindow;
store?: LocalStore;
src?: string;
};
// Store
declare interface LocalStore {
getWinBounds(): WinBounds;

View File

@@ -1,8 +1,24 @@
import path from 'path';
import isDev from 'electron-is-dev';
export const PROTOCOL = 'file';
export const RESOURCES = isDev
? path.join(__dirname, '..', 'public', 'static')
: process.resourcesPath;
export const MODULES = [
// Event Logging
{ name: 'event-logging/process', dependencies: [] },
{ name: 'event-logging/app', dependencies: [] },
{ name: 'event-logging/contents', dependencies: ['mainWindow'] },
// Standard modules
{ name: 'menu', dependencies: ['mainWindow'] },
{ name: 'shortcuts', dependencies: ['mainWindow', 'src'] },
{ name: 'request-filter', dependencies: ['mainWindow'] },
{ name: 'external-links', dependencies: ['mainWindow', 'store'] },
{ name: 'window-controls', dependencies: ['mainWindow', 'store'] },
{ name: 'http-receiver', dependencies: ['mainWindow', 'src'] },
{ name: 'metadata', dependencies: [] },
{ name: 'bridge', dependencies: [] },
{ name: 'tor', dependencies: ['mainWindow', 'store'] },
// Prod Only
{ name: 'csp', dependencies: ['mainWindow'], isDev: false },
{ name: 'file-protocol', dependencies: ['mainWindow', 'src'], isDev: false },
{ name: 'auto-updater', dependencies: ['mainWindow', 'store'], isDev: false },
// Dev Only
{ name: 'dev-tools', dependencies: ['mainWindow'], isDev: true },
];

View File

@@ -37,6 +37,7 @@ export class HttpReceiver extends EventEmitter {
pathname: string;
handler: (request: Request, response: http.ServerResponse) => void;
}[];
logger: ILogger;
// Possible ports
// todo: add more to prevent case when port is blocked
@@ -56,6 +57,7 @@ export class HttpReceiver extends EventEmitter {
*/
];
this.server = http.createServer(this.onRequest);
this.logger = global.logger;
}
getServerAddress() {
@@ -79,13 +81,13 @@ export class HttpReceiver extends EventEmitter {
return new Promise((resolve, reject) => {
this.server.on('error', e => {
// @ts-ignore - type is missing
if (e.code === 'EADDRINUSE') {
// todo:
}
this.logger.error('http-receiver', `Start error code: ${e.code}`);
// if (e.code === 'EADDRINUSE') {} // TODO: Try different port if already in use
this.server.close();
return reject();
});
this.server.listen(HttpReceiver.PORTS[0], '127.0.0.1', undefined, () => {
this.logger.info('http-receiver', 'Server started');
const address = this.getServerAddress();
if (address) {
this.emit('server/listening', address);
@@ -97,7 +99,9 @@ export class HttpReceiver extends EventEmitter {
stop() {
// note that this method only stops listening but keeps existing connections open and thus port blocked
this.server.close();
this.server.close(() => {
this.logger.info('http-receiver', 'Server stopped');
});
}
/**
@@ -106,6 +110,7 @@ export class HttpReceiver extends EventEmitter {
private onRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
// mostly ts stuff. request should always have url defined.
if (!request.url) {
this.logger.warn('http-receiver', 'Unexpected incoming message (no url)');
this.emit('server/error', 'Unexpected incoming message');
return;
}
@@ -114,15 +119,21 @@ export class HttpReceiver extends EventEmitter {
// only method GET is supported
if (request.method !== 'GET') {
this.logger.warn('http-receiver', `Incorrect method used (${request.method})`);
return;
}
const route = this.routes.find(r => r.pathname === pathname);
if (route) {
response.setHeader('Content-Type', 'text/html; charset=UTF-8');
// original type has url as optional, Request alias makes it required.
return route.handler(request as Request, response);
if (!route) {
this.logger.warn('http-receiver', `Route not found for ${pathname}`);
return;
}
this.logger.info('http-receiver', `Handling request for ${pathname}`);
response.setHeader('Content-Type', 'text/html; charset=UTF-8');
// original type has url as optional, Request alias makes it required.
return route.handler(request as Request, response);
};
// TODO: add option to auto-close after X seconds. Possible?

View File

@@ -0,0 +1,45 @@
import { app } from 'electron';
import isDev from 'electron-is-dev';
import si from 'systeminformation';
import { b2t } from '@lib/utils';
import { toHumanReadable } from '@suite/utils/suite/file';
export const buildInfo = () => {
global.logger.info('build', [
'Info:',
`- Version: ${app.getVersion()}`,
`- Commit: ${process.env.COMMITHASH}`,
`- Dev: ${b2t(isDev)}`,
`- Args: ${process.argv.slice(1).join(' ')}`,
`- CWD: ${process.cwd()}`,
]);
};
export const computerInfo = async () => {
const { logger } = global;
if (logger.level !== 'debug') {
return;
}
const { system, cpu, mem, osInfo } = await si.get({
system: 'manufacturer, model, virtual',
cpu: 'manufacturer, brand, processors, physicalCores, cores, speed',
mem: 'total',
osInfo: 'platform, arch, distro, release',
});
logger.debug('computer', [
'Info:',
`- Platform: ${osInfo.platform} ${osInfo.arch}`,
`- OS: ${osInfo.distro} ${osInfo.release}`,
`- Manufacturer: ${system.manufacturer}`,
`- Model: ${system.model}`,
`- Virtual: ${b2t(system.virtual)}`,
`- CPU: ${cpu.manufacturer} ${cpu.brand}`,
`- Cores: ${cpu.processors}x${cpu.physicalCores}(+${cpu.cores - cpu.physicalCores}) @ ${
cpu.speed
}GHz`,
`- RAM: ${toHumanReadable(mem.total)}`,
]);
};

View File

@@ -0,0 +1,138 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { app } from 'electron';
const logLevels = ['mute', 'error', 'warn', 'info', 'debug'] as const;
export type LogLevel = typeof logLevels[number];
export type Options = {
colors?: boolean; // Console output has colors
writeToConsole?: boolean; // Output is displayed in the console
writeToDisk?: boolean; // Output is written to a file
outputFile?: string; // file name for the output
outputPath?: string; // path for the output
logFormat?: string; // Output format of the log
};
const defaultOptions: Options = {
colors: true,
writeToConsole: true,
writeToDisk: false,
outputFile: 'log-%ts.txt',
outputPath: app?.getPath('logs') || process.cwd(),
logFormat: '%dt - %lvl(%top): %msg',
};
class Logger implements ILogger {
static instance: Logger;
private stream: fs.WriteStream;
private options: Options;
private logLevel = 0;
constructor(level: LogLevel, options?: Options) {
this.logLevel = logLevels.indexOf(level);
this.options = {
...defaultOptions,
...options,
};
if (this.logLevel > 0 && this.options.writeToDisk) {
this.stream = fs.createWriteStream(
path.join(this.options.outputPath, this.format(this.options.outputFile)),
);
}
}
private log(level: LogLevel, topic: string, message: string | string[]) {
const { writeToConsole, writeToDisk, logFormat } = this.options;
if (!writeToConsole && !writeToDisk) {
return;
}
const logLevel = logLevels.indexOf(level);
if (this.logLevel < logLevel) {
return;
}
const messages: string[] = typeof message === 'string' ? [message] : message;
messages.forEach(m =>
this.write(
level,
this.format(logFormat, {
lvl: level.toUpperCase(),
top: topic,
msg: m,
}),
),
);
}
private write(level: LogLevel, message: string) {
if (this.options.writeToConsole) {
console.log(this.options.colors ? this.color(level, message) : message);
}
if (this.options.writeToDisk) {
this.stream.write(`${message}\n`);
}
}
private format(format: string, strings: { [key: string]: string } = {}) {
let message = format;
Object.keys(strings).forEach(k => {
message = message.replace(`%${k}`, strings[k]);
});
message = message
.replace('%dt', new Date().toISOString())
.replace('%ts', (+new Date()).toString());
return message;
}
private color(level: LogLevel, message: string) {
switch (level) {
case 'error':
return chalk.red(message.replace('ERROR', chalk.bold('ERROR')));
case 'warn':
return message.replace('WARN', chalk.bold.yellow('WARN'));
case 'info':
return message.replace('INFO', chalk.bold.blue('INFO'));
case 'debug':
return message.replace('DEBUG', chalk.bold.cyan('DEBUG'));
default:
return message;
}
}
public exit() {
if (this.options.writeToDisk) {
this.stream.end();
}
}
public error(topic: string, message: string | string[]) {
this.log('error', topic, message);
}
public warn(topic: string, message: string | string[]) {
this.log('warn', topic, message);
}
public info(topic: string, message: string | string[]) {
this.log('info', topic, message);
}
public debug(topic: string, message: string | string[]) {
this.log('debug', topic, message);
}
public get level() {
return logLevels[this.logLevel];
}
}
export default Logger;

View File

@@ -0,0 +1,43 @@
import isDev from 'electron-is-dev';
import { MODULES } from '@lib/constants';
const modules = async (dependencies: Dependencies) => {
const { logger } = global;
logger.info('modules', `Loading ${MODULES.length} modules`);
await Promise.all(
MODULES.flatMap(async module => {
if (module.isDev !== undefined && module.isDev !== isDev) {
logger.debug(
'modules',
`${module.name} was skipped because it is configured for a diferent environment`,
);
return [];
}
const deps: { [name in keyof Dependencies]: any } = {};
module.dependencies.forEach((dep: keyof Dependencies) => {
if (dependencies[dep] === undefined) {
logger.error(
'modules',
`Dependency ${dep} is missing for module ${module.name}`,
);
throw new Error(); // TODO
}
deps[dep] = dependencies[dep];
});
try {
const m = await import(`./modules/${module.name}`);
return [m.default(deps)];
} catch (err) {
logger.error('modules', `Couldn't load ${module.name} (${err.toString()})`);
}
}),
);
logger.info('modules', 'All modules loaded');
};
export default modules;

View File

@@ -1,7 +1,7 @@
import path from 'path';
import { spawn, ChildProcess } from 'child_process';
import isDev from 'electron-is-dev';
import { RESOURCES } from '../constants';
import { b2t } from '@lib/utils';
export type Status = {
service: boolean;
@@ -33,6 +33,8 @@ abstract class BaseProcess {
startupThrottle: ReturnType<typeof setTimeout> | null;
supportedSystems = ['linux-arm64', 'linux-x64', 'mac-x64', 'win-x64'];
stopped = false;
logger: ILogger;
logTopic: string;
/**
* @param resourceName Resource folder name
@@ -48,6 +50,15 @@ abstract class BaseProcess {
...defaultOptions,
...options,
};
const { logger } = global;
this.logger = logger;
this.logTopic = `process-${this.processName}`;
const { system } = this.getPlatformInfo();
if (!this.isSystemSupported(system)) {
throw new Error(`[${this.resourceName}] unsupported system (${system})`);
}
}
/**
@@ -63,6 +74,7 @@ abstract class BaseProcess {
*/
async start(params: string[] = []) {
if (this.startupThrottle) {
this.logger.warn(this.logTopic, 'Canceling process start (throttle)');
return;
}
@@ -70,30 +82,38 @@ abstract class BaseProcess {
// Service is running, nothing to do
if (status.service) {
this.logger.warn(this.logTopic, 'Canceling process start (service running)');
return;
}
// If the process is running but the service isn't
if (status.process) {
this.logger.warn(this.logTopic, 'Process is running but service is not');
// Stop the process
await this.stop();
}
// Throttle process start
if (this.options.startupCooldown && this.options.startupCooldown > 0) {
this.logger.debug(
this.logTopic,
`Setting a restart throttle (${this.options.startupCooldown})`,
);
this.startupThrottle = setTimeout(() => {
this.logger.debug(this.logTopic, 'Clearing throttle');
this.startupThrottle = null;
}, this.options.startupCooldown * 1000);
}
const { system, ext } = this.getPlatformInfo();
if (!this.isSystemSupported(system)) {
throw new Error(`[${this.resourceName}] unsupported system (${system})`);
}
this.stopped = false;
const processDir = path.join(RESOURCES, 'bin', this.resourceName, isDev ? system : '');
const { system, ext } = this.getPlatformInfo();
const processDir = path.join(
global.resourcesPath,
'bin',
this.resourceName,
isDev ? system : '',
);
const processPath = path.join(processDir, `${this.processName}${ext}`);
const processEnv = { ...process.env };
// library search path for macOS
@@ -104,6 +124,13 @@ abstract class BaseProcess {
processEnv.LD_LIBRARY_PATH = processEnv.LD_LIBRARY_PATH
? `${processEnv.LD_LIBRARY_PATH}:${processDir}`
: `${processDir}`;
this.logger.info(this.logTopic, [
'Starting process:',
`- Path: ${processPath}`,
`- Params: ${params}`,
`- CWD: ${processDir}`,
]);
this.process = spawn(processPath, params, {
cwd: processDir,
env: processEnv,
@@ -117,27 +144,32 @@ abstract class BaseProcess {
*/
stop() {
return new Promise<void>(resolve => {
this.stopped = true;
if (!this.process) {
this.stopped = true;
this.logger.warn(this.logTopic, "Couldn't stop process (already stopped)");
resolve();
return;
}
this.logger.info(this.logTopic, 'Stopping process');
this.process.kill();
let timeout = 0;
const interval = setInterval(() => {
if (!this.process || this.process.killed) {
this.logger.info(this.logTopic, 'Killed successfully');
clearInterval(interval);
this.process = null;
this.stopped = true;
resolve();
return;
}
if (this.options.stopKillWait && timeout < this.options.stopKillWait) {
this.logger.info(this.logTopic, 'Still alive, checking again...');
timeout++;
} else {
this.logger.info(this.logTopic, 'Still alive, going for the SIGKILL');
this.process.kill('SIGKILL');
}
}, 1000);
@@ -149,17 +181,20 @@ abstract class BaseProcess {
* @param force Force the restart
*/
async restart() {
this.logger.info(this.logTopic, 'Restarting');
await this.stop();
await this.start();
}
onError(err: Error) {
console.error('ERROR', this.processName, err);
this.logger.error(this.logTopic, err.message);
}
onExit() {
this.logger.info(this.logTopic, `Exited (Stopped: ${b2t(this.stopped)})`);
this.process = null;
if (this.options.autoRestart && this.options.autoRestart > 0 && !this.stopped) {
this.logger.debug(this.logTopic, 'Auto restarting...');
setTimeout(() => this.start(), this.options.autoRestart * 1000);
}
}

View File

@@ -18,6 +18,7 @@ class BridgeProcess extends BaseProcess {
Origin: 'https://electron.trezor.io',
},
});
this.logger.debug(this.logTopic, `Checking status (${resp.status})`);
if (resp.status === 200) {
const data = await resp.json();
if (data?.version) {
@@ -27,8 +28,8 @@ class BridgeProcess extends BaseProcess {
};
}
}
} catch {
//
} catch (err) {
this.logger.error(this.logTopic, `Status error: ${err.message}`);
}
// process
@@ -39,7 +40,7 @@ class BridgeProcess extends BaseProcess {
}
async startDev(): Promise<void> {
await this.start(['-e', '21324']);
await super.start(['-e', '21324']);
}
}

View File

@@ -10,6 +10,7 @@ export const BUFFER = 0.2;
// Functions
export const getInitialWindowSize = () => {
const { bounds } = screen.getPrimaryDisplay();
const buffer = {
width: bounds.width * BUFFER,
height: bounds.height * BUFFER,

View File

@@ -15,6 +15,7 @@ export const save = async (directory: string, name: string, content: string) =>
await fs.promises.writeFile(file, content, 'utf-8');
return { success: true };
} catch (error) {
global.logger.error('user-data', `Save failed: ${error.message}`);
return { success: false, error };
}
};
@@ -33,6 +34,7 @@ export const read = async (directory: string, name: string) => {
const payload = await fs.promises.readFile(file, 'utf-8');
return { success: true, payload };
} catch (error) {
global.logger.error('user-data', `Read failed: ${error.message}`);
return { success: false, error };
}
};

View File

@@ -15,3 +15,6 @@ export const isPortUsed = (port: number) =>
server.listen(port);
});
// Bool 2 Text
export const b2t = (bool: boolean) => (bool ? 'Yes' : 'No');

View File

@@ -2,13 +2,17 @@
* Auto Updater feature (notify, download, install)
*/
import { app, ipcMain, BrowserWindow } from 'electron';
import { app, ipcMain } from 'electron';
import { autoUpdater, CancellationToken } from 'electron-updater';
import { b2t } from '@lib/utils';
import { toHumanReadable } from '@suite/utils/suite/file';
// Runtime flags
const preReleaseFlag = app.commandLine.hasSwitch('pre-release');
const init = (window: BrowserWindow, store: LocalStore) => {
const init = ({ mainWindow, store }: Dependencies) => {
const { logger } = global;
let isManualCheck = false;
let latestVersion = {};
let updateCancellationToken: CancellationToken;
@@ -17,45 +21,82 @@ const init = (window: BrowserWindow, store: LocalStore) => {
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.allowPrerelease = preReleaseFlag;
autoUpdater.logger = null;
logger.info('auto-updater', `Is looking for pre-releases? (${b2t(preReleaseFlag)})`);
if (updateSettings.skipVersion) {
window.webContents.send('update/skip', updateSettings.skipVersion);
logger.debug('auto-updater', `Set to skip version ${updateSettings.skipVersion}`);
mainWindow.webContents.send('update/skip', updateSettings.skipVersion);
}
autoUpdater.on('checking-for-update', () => {
window.webContents.send('update/checking');
logger.info('auto-updater', 'Checking for update');
mainWindow.webContents.send('update/checking');
});
autoUpdater.on('update-available', ({ version, releaseDate }) => {
if (updateSettings.skipVersion === version) {
logger.warn('auto-updater', [
'Update is available but was skipped:',
`- Update version: ${version}`,
`- Skip version: ${updateSettings.skipVersion}`,
]);
return;
}
logger.warn('auto-updater', [
'Update is available:',
`- Update version: ${version}`,
`- Release date: ${releaseDate}`,
`- Manual check: ${b2t(isManualCheck)}`,
]);
latestVersion = { version, releaseDate, isManualCheck };
window.webContents.send('update/available', latestVersion);
mainWindow.webContents.send('update/available', latestVersion);
// Reset manual check flag
isManualCheck = false;
});
autoUpdater.on('update-not-available', ({ version, releaseDate }) => {
logger.info('auto-updater', [
'No new update is available:',
`- Last version: ${version}`,
`- Last release date: ${releaseDate}`,
`- Manual check: ${b2t(isManualCheck)}`,
]);
latestVersion = { version, releaseDate, isManualCheck };
window.webContents.send('update/not-available', latestVersion);
mainWindow.webContents.send('update/not-available', latestVersion);
// Reset manual check flag
isManualCheck = false;
});
autoUpdater.on('error', err => {
window.webContents.send('update/error', err);
logger.error('auto-updater', `An error happened: ${err.toString()}`);
mainWindow.webContents.send('update/error', err);
});
autoUpdater.on('download-progress', progressObj => {
window.webContents.send('update/downloading', { ...progressObj });
logger.debug(
'auto-updater',
`Downloading ${progressObj.percent}% (${toHumanReadable(
progressObj.transferred,
)}/${toHumanReadable(progressObj.total)})`,
);
mainWindow.webContents.send('update/downloading', { ...progressObj });
});
autoUpdater.on('update-downloaded', ({ version, releaseDate, downloadedFile }) => {
window.webContents.send('update/downloaded', { version, releaseDate, downloadedFile });
logger.info('auto-updater', [
'Update downloaded:',
`- Last version: ${version}`,
`- Last release date: ${releaseDate}`,
`- Downloaded file: ${downloadedFile}`,
]);
mainWindow.webContents.send('update/downloaded', { version, releaseDate, downloadedFile });
});
ipcMain.on('update/check', (_, isManual?: boolean) => {
@@ -63,19 +104,25 @@ const init = (window: BrowserWindow, store: LocalStore) => {
isManualCheck = true;
}
logger.info('auto-updater', `Update checking request (manual: ${b2t(isManualCheck)})`);
autoUpdater.checkForUpdates();
});
ipcMain.on('update/download', () => {
window.webContents.send('update/downloading', {
logger.info('auto-updater', 'Download requested');
mainWindow.webContents.send('update/downloading', {
percent: 0,
bytesPerSecond: 0,
total: 0,
transferred: 0,
});
updateCancellationToken = new CancellationToken();
autoUpdater.downloadUpdate(updateCancellationToken).catch(() => null); // Suppress error in console
autoUpdater
.downloadUpdate(updateCancellationToken)
.then(() => logger.info('auto-updater', 'Update cancelled'))
.catch(() => null); // Suppress error in console
});
ipcMain.on('update/install', () => {
logger.info('auto-updater', 'Installation request');
// This will force the closing of the window to quit the app on Mac
global.quitOnWindowClose = true;
// https://www.electron.build/auto-update#module_electron-updater.AppUpdater+quitAndInstall
@@ -85,11 +132,13 @@ const init = (window: BrowserWindow, store: LocalStore) => {
autoUpdater.quitAndInstall(true, true);
});
ipcMain.on('update/cancel', () => {
window.webContents.send('update/available', latestVersion);
logger.info('auto-updater', 'Cancel update request');
mainWindow.webContents.send('update/available', latestVersion);
updateCancellationToken.cancel();
});
ipcMain.on('update/skip', (_, version) => {
window.webContents.send('update/skip', version);
logger.info('auto-updater', `Skip version (${version}) request`);
mainWindow.webContents.send('update/skip', version);
updateSettings.skipVersion = version;
store.setUpdateSettings(updateSettings);
});

View File

@@ -3,32 +3,40 @@
*/
import { app, session } from 'electron';
import BridgeProcess from '@lib/processes/BridgeProcess';
import { b2t } from '@lib/utils';
const filter = {
urls: ['http://127.0.0.1:21325/*'],
};
const bridgeDev = app.commandLine.hasSwitch('bridge-dev');
const bridge = new BridgeProcess();
const init = async () => {
const { logger } = global;
const bridge = new BridgeProcess();
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
// @ts-ignore electron declares requestHeaders as an empty interface
details.requestHeaders.Origin = 'https://electron.trezor.io';
logger.debug('bridge', `Setting header for ${details.url}`);
callback({ cancel: false, requestHeaders: details.requestHeaders });
});
try {
logger.info('bridge', `Starting (Dev: ${b2t(bridgeDev)})`);
if (bridgeDev) {
await bridge.startDev();
} else {
await bridge.start();
}
} catch {
//
} catch (err) {
logger.error('bridge', `Start failed: ${err.message}`);
}
app.on('before-quit', () => bridge.stop());
app.on('before-quit', () => {
logger.info('bridge', 'Stopping (app quit)');
bridge.stop();
});
};
export default init;

View File

@@ -2,15 +2,18 @@
* Adds a CSP (Content Security Policy) header to all requests
*/
import { app, dialog, BrowserWindow, session } from 'electron';
import { app, dialog, session } from 'electron';
import * as config from '../config';
const disableCspFlag = app.commandLine.hasSwitch('disable-csp');
const init = (window: BrowserWindow) => {
const init = ({ mainWindow }: Dependencies) => {
const { logger } = global;
if (disableCspFlag) {
dialog.showMessageBox(window, {
logger.warn('csp', 'The application was launched with CSP disabled');
dialog.showMessageBox(mainWindow, {
type: 'warning',
message:
'The application is running with CSP disabled. This is a security risk! If this is not intentional, please close the application immediately.',
@@ -18,6 +21,7 @@ const init = (window: BrowserWindow) => {
});
} else {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
logger.debug('csp', `Header applied to ${details.url}`);
callback({
responseHeaders: {
'Content-Security-Policy': [config.cspRules.join(';')],

View File

@@ -1,11 +1,9 @@
/**
* Enable development tools
*/
import { BrowserWindow } from 'electron';
const init = (window: BrowserWindow) => {
window.webContents.once('dom-ready', () => {
window.webContents.openDevTools();
const init = ({ mainWindow }: Dependencies) => {
mainWindow.webContents.once('dom-ready', () => {
mainWindow.webContents.openDevTools();
});
};

View File

@@ -0,0 +1,23 @@
import { app } from 'electron';
const init = () => {
const { logger } = global;
app.on('ready', () => {
logger.info('app', 'Ready');
});
app.on('before-quit', () => {
logger.info('app', 'Quitting');
});
app.on('window-all-closed', () => {
logger.info('app', 'All windows closed');
});
app.on('child-process-gone', (_, { type, reason }) => {
logger.error('app', `Child process (${type}) gone (reason: ${reason})`);
});
};
export default init;

View File

@@ -0,0 +1,63 @@
const init = ({ mainWindow }: Dependencies) => {
const { logger } = global;
mainWindow.webContents.on('did-fail-load', (_, errorCode, errorDescription, validatedUrl) => {
logger.error(
'content',
`Failure to load ${validatedUrl} (${errorCode} - ${errorDescription})`,
);
});
mainWindow.webContents.on('will-navigate', (_, url) => {
logger.info('content', `Navigate to ${url}`);
});
mainWindow.webContents.on('render-process-gone', (_, { reason }) => {
logger.error('content', `Render process gone (reason: ${reason}`);
});
let unresponsiveStart = 0;
mainWindow.webContents.on('unresponsive', () => {
unresponsiveStart = +new Date();
logger.warn('content', 'Unresponsive');
});
mainWindow.webContents.on('responsive', () => {
if (unresponsiveStart !== 0) {
logger.warn(
'content',
`Responsive again after ${(+new Date() - unresponsiveStart / 1000).toFixed(1)}s`,
);
unresponsiveStart = 0;
}
});
mainWindow.webContents.on('devtools-opened', () => {
logger.info('content', `Dev tools opened`);
});
mainWindow.webContents.on('devtools-closed', () => {
logger.info('content', `Dev tools closed`);
});
mainWindow.webContents.on('console-message', (_, level, message, line, sourceId) => {
const msg = `${message} - ${sourceId}:${line}`;
switch (level) {
case 0:
logger.debug('console-log', msg);
break;
case 1:
logger.info('console-log', msg);
break;
case 2:
logger.warn('console-log', msg);
break;
case 3:
logger.error('console-log', msg);
break;
// no default
}
});
};
export default init;

View File

@@ -0,0 +1,13 @@
const init = () => {
const { logger } = global;
process.on('uncaughtException', e => {
logger.error('exception', e.message);
});
process.on('unhandledRejection', e => {
logger.warn('rejection', `Unhandled Rejection: ${e.toString()}`);
});
};
export default init;

View File

@@ -1,37 +1,48 @@
/**
* Opens external links in the default browser (displays a warning when using Tor)
*/
import { shell, dialog, BrowserWindow } from 'electron';
import { shell, dialog } from 'electron';
import * as config from '../config';
const init = (window: BrowserWindow, store: LocalStore) => {
const init = ({ mainWindow, store }: Dependencies) => {
const { logger } = global;
const handleExternalLink = (event: Event, url: string) => {
if (config.oauthUrls.some(u => url.startsWith(u))) {
event.preventDefault();
logger.info('external-links', `${url} was allowed (OAuth list)`);
return shell.openExternal(url);
}
if (url !== window.webContents.getURL()) {
if (url !== mainWindow.webContents.getURL()) {
event.preventDefault();
const torSettings = store.getTorSettings();
if (torSettings.running) {
// TODO: Replace with in-app modal
const result = dialog.showMessageBoxSync(window, {
const result = dialog.showMessageBoxSync(mainWindow, {
type: 'warning',
message: `The following URL is going to be opened in your browser\n\n${url}`,
buttons: ['Cancel', 'Continue'],
});
const cancel = result === 0;
logger.info(
'external-links',
`${url} was ${cancel ? 'not ' : ''}allowed by user in TOR mode`,
);
// Cancel
if (result === 0) return;
if (cancel) return;
}
logger.info('external-links', `${url} opened in default browser`);
shell.openExternal(url);
}
};
window.webContents.on('new-window', handleExternalLink);
window.webContents.on('will-navigate', handleExternalLink);
mainWindow.webContents.on('new-window', handleExternalLink);
mainWindow.webContents.on('will-navigate', handleExternalLink);
};
export default init;

View File

@@ -2,21 +2,21 @@
* Helps pointing to the right folder to load
*/
import path from 'path';
import { session, BrowserWindow } from 'electron';
import { session } from 'electron';
import { PROTOCOL } from '@lib/constants';
const init = (window: BrowserWindow, src: string) => {
const init = ({ mainWindow, src }: Dependencies) => {
// Point to the right directory for file protocol requests
session.defaultSession.protocol.interceptFileProtocol(PROTOCOL, (request, callback) => {
let url = request.url.substr(PROTOCOL.length + 1);
url = path.join(__dirname, '..', 'build', url);
url = path.join(__dirname, '..', '..', 'build', url);
callback(url);
});
// Refresh if it failed to load
window.webContents.on('did-fail-load', () => {
window.loadURL(src);
mainWindow.webContents.on('did-fail-load', () => {
mainWindow.loadURL(src);
});
};

View File

@@ -1,7 +1,7 @@
/**
* Local web server for handling requests to app
*/
import { app, ipcMain, BrowserWindow } from 'electron';
import { app, ipcMain } from 'electron';
import { buyRedirectHandler } from '@lib/buy';
import { HttpReceiver } from '@lib/http-receiver';
@@ -9,31 +9,35 @@ import { HttpReceiver } from '@lib/http-receiver';
// External request handler
const httpReceiver = new HttpReceiver();
const init = (window: BrowserWindow, src: string) => {
const init = ({ mainWindow, src }: Dependencies) => {
const { logger } = global;
// wait for httpReceiver to start accepting connections then register event handlers
httpReceiver.on('server/listening', () => {
// when httpReceiver accepted oauth response
httpReceiver.on('oauth/response', message => {
window.webContents.send('oauth/response', message);
mainWindow.webContents.send('oauth/response', message);
app.focus();
});
httpReceiver.on('buy/redirect', url => {
buyRedirectHandler(url, window, src);
buyRedirectHandler(url, mainWindow, src);
});
httpReceiver.on('spend/message', event => {
window.webContents.send('spend/message', event);
mainWindow.webContents.send('spend/message', event);
});
// when httpReceiver was asked to provide current address for given pathname
ipcMain.handle('server/request-address', (_event, pathname) =>
ipcMain.handle('server/request-address', (_, pathname) =>
httpReceiver.getRouteAddress(pathname),
);
});
logger.info('http-receiver', 'Starting server');
httpReceiver.start();
app.on('before-quit', () => {
logger.info('http-receiver', 'Stopping server (app quit)');
httpReceiver.stop();
});
};

View File

@@ -1,16 +1,26 @@
import { BrowserWindow, Menu } from 'electron';
import { Menu } from 'electron';
import { buildMainMenu, inputMenu, selectionMenu } from '@lib/menu';
import { b2t } from '@lib/utils';
const init = ({ mainWindow }: Dependencies) => {
const { logger } = global;
const init = (window: BrowserWindow) => {
Menu.setApplicationMenu(buildMainMenu());
window.setMenuBarVisibility(false);
mainWindow.setMenuBarVisibility(false);
mainWindow.webContents.on('context-menu', (_, props) => {
const isTextSelected = props.selectionText && props.selectionText.trim() !== '';
logger.debug('menu', [
'Context menu:',
`- Editable: ${b2t(props.isEditable)}`,
`- Text Selected: ${b2t(isTextSelected)}`,
]);
window.webContents.on('context-menu', (_e, props) => {
if (props.isEditable) {
// right click on the input/textarea should open a context menu with text editing options (copy, cut, paste,...)
inputMenu.popup();
} else if (props.selectionText && props.selectionText.trim() !== '') {
} else if (isTextSelected) {
// right click with active text selection should open context menu with a copy option
selectionMenu.popup();
}

View File

@@ -7,12 +7,16 @@ import { save, read } from '@lib/user-data';
const DATA_DIR = '/metadata';
export const init = () => {
const { logger } = global;
ipcMain.handle('metadata/write', async (_, message) => {
logger.info('metadata', `Writing metadata to ${DATA_DIR}/${message.file}`);
const resp = await save(DATA_DIR, message.file, message.content);
return resp;
});
ipcMain.handle('metadata/read', async (_, message) => {
logger.info('metadata', `Reading metadata from ${DATA_DIR}/${message.file}`);
const resp = await read(DATA_DIR, message.file);
return resp;
});

View File

@@ -1,37 +1,49 @@
/**
* Request Filter feature (blocks non-allowed requests and displays a warning)
*/
import { dialog, session, BrowserWindow } from 'electron';
import { dialog, session } from 'electron';
import * as config from '../config';
const init = (window: BrowserWindow) => {
const init = ({ mainWindow }: Dependencies) => {
const { logger } = global;
const resourceTypeFilter = ['xhr']; // What resource types we want to filter
const caughtDomainExceptions: string[] = []; // Domains that have already shown an exception
session.defaultSession.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, cb) => {
if (!resourceTypeFilter.includes(details.resourceType)) {
logger.debug(
'request-filter',
`${details.url} was allowed because its resource type (${details.resourceType}) is not filtered`,
);
cb({ cancel: false });
return;
}
const { hostname } = new URL(details.url);
// Cancel requests that aren't allowed
if (config.allowedDomains.find(d => hostname.endsWith(d)) !== undefined) {
logger.info(
'request-filter',
`${details.url} was allowed because ${hostname} is in the exception list`,
);
cb({ cancel: false });
return;
}
if (caughtDomainExceptions.find(d => d === hostname) === undefined) {
caughtDomainExceptions.push(hostname);
dialog.showMessageBox(window, {
dialog.showMessageBox(mainWindow, {
type: 'warning',
message: `Suite blocked a request to ${hostname}.\n\nIf you believe this is an error, please contact our support.`,
buttons: ['OK'],
});
}
console.warn(`[Warning] Domain '${hostname}' was blocked.`);
logger.warn(
'request-filter',
`${details.url} was blocked because ${hostname} is not in the exception list`,
);
cb({ cancel: true });
});
};

View File

@@ -1,21 +1,26 @@
import { app, BrowserWindow } from 'electron';
import { app } from 'electron';
import electronLocalshortcut from 'electron-localshortcut';
const init = (window: BrowserWindow, src: string) => {
electronLocalshortcut.register(window, 'CommandOrControl+Alt+I', () => {
window.webContents.openDevTools();
const init = ({ mainWindow, src }: Dependencies) => {
const { logger } = global;
electronLocalshortcut.register(mainWindow, 'CommandOrControl+Alt+I', () => {
logger.info('shortcuts', 'CTRL+ALT+I pressed');
mainWindow.webContents.openDevTools();
});
electronLocalshortcut.register(window, 'F5', () => {
window.loadURL(src);
electronLocalshortcut.register(mainWindow, 'F5', () => {
logger.info('shortcuts', 'F5 pressed');
mainWindow.loadURL(src);
});
electronLocalshortcut.register(window, 'CommandOrControl+R', () => {
window.loadURL(src);
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
logger.info('shortcuts', 'CTRL+R pressed');
mainWindow.loadURL(src);
});
app.on('before-quit', () => {
electronLocalshortcut.unregisterAll(window);
electronLocalshortcut.unregisterAll(mainWindow);
});
};

View File

@@ -1,49 +1,68 @@
/**
* Tor feature (toggle, configure)
*/
import { app, session, ipcMain, BrowserWindow, IpcMainEvent } from 'electron';
import { app, session, ipcMain, IpcMainEvent } from 'electron';
import TorProcess from '@lib/processes/TorProcess';
import { b2t } from '@lib/utils';
import { onionDomain } from '../config';
const tor = new TorProcess();
const torFlag = app.commandLine.hasSwitch('tor');
const init = async (window: BrowserWindow, store: LocalStore) => {
const init = async ({ mainWindow, store }: Dependencies) => {
const { logger } = global;
const tor = new TorProcess();
const torSettings = store.getTorSettings();
const toggleTor = async (start: boolean) => {
if (start) {
if (torSettings.running) {
logger.info('tor', 'Restarting');
await tor.restart();
} else {
logger.info('tor', 'Starting');
await tor.start();
}
} else {
logger.info('tor', 'Stopping');
await tor.stop();
}
torSettings.running = start;
store.setTorSettings(torSettings);
window.webContents.send('tor/status', start);
mainWindow.webContents.send('tor/status', start);
const proxy = start ? `socks5://${torSettings.address}` : '';
logger.info('tor', `Setting proxy to "${proxy}"`);
session.defaultSession.setProxy({
proxyRules: start ? `socks5://${torSettings.address}` : '',
proxyRules: proxy,
});
};
if (torFlag || torSettings.running) {
logger.info('tor', [
'Auto starting:',
`- Running with flag: ${b2t(torFlag)}`,
`- Running with settings: ${b2t(torSettings.running)}`,
]);
await toggleTor(true);
}
ipcMain.on('tor/toggle', async (_, start: boolean) => {
logger.info('tor', `Toggling ${start ? 'ON' : 'OFF'}`);
await toggleTor(start);
});
ipcMain.on('tor/set-address', () => async (_: IpcMainEvent, address: string) => {
if (torSettings.address !== address) {
logger.debug('tor', [
'Updating address:',
`- From: ${torSettings.address}`,
`- To: ${address}`,
]);
torSettings.address = address;
store.setTorSettings(torSettings);
@@ -54,10 +73,12 @@ const init = async (window: BrowserWindow, store: LocalStore) => {
});
ipcMain.on('tor/get-status', () => {
window.webContents.send('tor/status', torSettings.running);
logger.debug('tor', `Getting status (${torSettings.running ? 'ON' : 'OFF'})`);
mainWindow.webContents.send('tor/status', torSettings.running);
});
ipcMain.handle('tor/get-address', () => {
logger.debug('tor', `Getting address (${torSettings.address})`);
return torSettings.address;
});
@@ -66,6 +87,7 @@ const init = async (window: BrowserWindow, store: LocalStore) => {
// Redirect outgoing trezor.io requests to .onion domain
if (torSettings.running && hostname.endsWith('trezor.io') && protocol === 'https:') {
logger.info('tor', `Rewriting ${details.url} to .onion URL`);
cb({
redirectURL: details.url.replace(
/https:\/\/(([a-z0-9]+\.)*)trezor\.io(.*)/,

View File

@@ -15,93 +15,121 @@ const notifyWindowActive = (window: BrowserWindow, state: boolean) => {
window.webContents.send('window/is-active', state);
};
const init = (window: BrowserWindow, store: LocalStore) => {
const init = ({ mainWindow, store }: Dependencies) => {
const { logger } = global;
if (process.platform === 'darwin') {
// macOS specific window behavior
// it is common for applications and their context menu to stay active until the user quits explicitly
// with Cmd + Q or right-click > Quit from the context menu.
// restore window after click on the Dock icon
app.on('activate', () => window.show());
app.on('activate', () => {
logger.info('window-control', 'Showing main window on activate');
mainWindow.show();
});
// hide window to the Dock
// this event listener will be removed by app.on('before-quit')
window.on('close', event => {
mainWindow.on('close', event => {
if (global.quitOnWindowClose) {
logger.info(
'window-control',
'Force quitting the app after the main window has been closed',
);
app.quit();
return;
}
logger.info('window-control', 'Hiding the app after the main window has been closed');
event.preventDefault();
window.hide();
mainWindow.hide();
});
} else {
// other platform just kills the app
app.on('window-all-closed', () => app.quit());
app.on('window-all-closed', () => {
logger.info('window-control', 'Quitting app after all windows have been closed');
app.quit();
});
}
window.on('page-title-updated', evt => {
mainWindow.on('page-title-updated', evt => {
// prevent updating window title
evt.preventDefault();
});
window.on('maximize', () => {
notifyWindowMaximized(window);
mainWindow.on('maximize', () => {
logger.debug('window-control', 'Maximize');
notifyWindowMaximized(mainWindow);
});
window.on('unmaximize', () => {
notifyWindowMaximized(window);
mainWindow.on('unmaximize', () => {
logger.debug('window-control', 'Unmaximize');
notifyWindowMaximized(mainWindow);
});
window.on('enter-full-screen', () => {
notifyWindowMaximized(window);
mainWindow.on('enter-full-screen', () => {
logger.debug('window-control', 'Enter full screen');
notifyWindowMaximized(mainWindow);
});
window.on('leave-full-screen', () => {
notifyWindowMaximized(window);
mainWindow.on('leave-full-screen', () => {
logger.debug('window-control', 'Leave full screen');
notifyWindowMaximized(mainWindow);
});
window.on('moved', () => {
notifyWindowMaximized(window);
mainWindow.on('moved', () => {
logger.debug('window-control', 'Moved');
notifyWindowMaximized(mainWindow);
});
window.on('focus', () => {
notifyWindowActive(window, true);
mainWindow.on('focus', () => {
logger.debug('window-control', 'Focus');
notifyWindowActive(mainWindow, true);
});
window.on('blur', () => {
notifyWindowActive(window, false);
mainWindow.on('blur', () => {
logger.debug('window-control', 'Blur');
notifyWindowActive(mainWindow, false);
});
ipcMain.on('window/close', () => {
logger.debug('window-control', 'Close requested');
// Keeping the devtools open might prevent the app from closing
if (window.webContents.isDevToolsOpened()) {
window.webContents.closeDevTools();
if (mainWindow.webContents.isDevToolsOpened()) {
mainWindow.webContents.closeDevTools();
}
// store window bounds on close btn click
const winBound = window.getBounds() as WinBounds;
const winBound = mainWindow.getBounds() as WinBounds;
store.setWinBounds(winBound);
window.close();
mainWindow.close();
});
ipcMain.on('window/minimize', () => {
window.minimize();
logger.debug('window-control', 'Minimize requested');
mainWindow.minimize();
});
ipcMain.on('window/maximize', () => {
logger.debug('window-control', 'Maximize requested');
if (process.platform === 'darwin') {
window.setFullScreen(true);
mainWindow.setFullScreen(true);
} else {
window.maximize();
mainWindow.maximize();
}
});
ipcMain.on('window/unmaximize', () => {
logger.debug('window-control', 'Unmaximize requested');
if (process.platform === 'darwin') {
window.setFullScreen(false);
mainWindow.setFullScreen(false);
} else {
window.unmaximize();
mainWindow.unmaximize();
}
});
ipcMain.on('client/ready', () => {
notifyWindowMaximized(window);
});
ipcMain.on('window/focus', () => {
logger.debug('window-control', 'Focus requested');
app.focus({ steal: true });
});
ipcMain.on('client/ready', () => {
logger.debug('window-control', 'Client ready');
notifyWindowMaximized(mainWindow);
});
app.on('before-quit', () => {
// store window bounds on cmd/ctrl+q
const winBound = window.getBounds() as WinBounds;
const winBound = mainWindow.getBounds() as WinBounds;
store.setWinBounds(winBound);
});
};

View File

@@ -20959,6 +20959,11 @@ symbol.prototype.description@^1.0.0:
has-symbols "^1.0.1"
object.getownpropertydescriptors "^2.1.0"
systeminformation@^4.34.7:
version "4.34.7"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.34.7.tgz#8b4a00c44781349905881c95dded8bafe7aae47f"
integrity sha512-cS3FiSZasFgVNjO9CP3aZmTO2VHwXKG+JN6Z85nWRyOzxRMNbZe7Xzwrewp42hj+OPMC3hk7MrAFyu/qLM65Mw==
table@^6.0.3, table@^6.0.4:
version "6.0.7"
resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34"