Centralize log file and refactor desktop logging
- Migrate to `electron-log` v5.X.X, centralizing log files to adhere to best-practices. - Add critical event logging in the log file. - Replace `ElectronLog` type with `LogFunctions` for better abstraction. - Unify log handling in `desktop-runtime-error` by removing `renderer.log` due to `electron-log` v5 changes. - Update and extend logger interfaces, removing 'I' prefix and adding common log levels to abstract `electron-log` completely. - Move logger interfaces to the application layer as it's cross-cutting concern, meanwhile keeping the implementations in the infrastructure layer. - Introduce `useLogger` hook for easier logging in Vue components. - Simplify `WindowVariables` by removing nullable properties. - Improve documentation to clearly differentiate between desktop and web versions, outlining specific features of each.
This commit is contained in:
@@ -124,9 +124,9 @@
|
|||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-Setup-0.12.8.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-Setup-0.12.8.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
||||||
|
|
||||||
💡 You should apply your configuration from time to time (more than once). It would strengthen your privacy and security control because privacy.sexy and its scripts get better and stronger in every new version.
|
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
|
|||||||
36
docs/desktop-vs-web-features.md
Normal file
36
docs/desktop-vs-web-features.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Desktop vs. Web Features
|
||||||
|
|
||||||
|
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||||
|
|
||||||
|
| Feature | Desktop | Web |
|
||||||
|
| ------- |---------|-----|
|
||||||
|
| [Usage without installation](#usage-without-installation) | 🔴 Not available | 🟢 Available |
|
||||||
|
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||||
|
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||||
|
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||||
|
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||||
|
|
||||||
|
## Feature Descriptions
|
||||||
|
|
||||||
|
### Usage without installation
|
||||||
|
|
||||||
|
The web version can be used directly in a browser without any installation, whereas the desktop version requires downloading and installing the software.
|
||||||
|
|
||||||
|
> **Note for Linux:** For Linux users, privacy.sexy is available as an AppImage, which is a portable format that does not require traditional installation. This means Linux users can use the desktop version without installation, similar to the web version.
|
||||||
|
|
||||||
|
### Offline usage
|
||||||
|
|
||||||
|
Once loaded, the web version can be used offline. The desktop version inherently supports offline usage.
|
||||||
|
|
||||||
|
### Auto-updates
|
||||||
|
|
||||||
|
Both versions automatically update to ensure you have the latest features and security enhancements.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
The desktop version supports logging of activities to aid in troubleshooting. This feature is not available in the web version.
|
||||||
|
|
||||||
|
### Script execution
|
||||||
|
|
||||||
|
Direct execution of scripts is possible in the desktop version, offering a more integrated experience.
|
||||||
|
This functionality is not present in the web version due to browser limitations.
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -6,14 +6,14 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.7",
|
"version": "0.12.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.30.0",
|
"ace-builds": "^1.30.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^5.0.1",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -6699,9 +6699,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-log": {
|
"node_modules/electron-log": {
|
||||||
"version": "4.4.8",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.1.tgz",
|
||||||
"integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
|
"integrity": "sha512-x4wnwHg00h/onWQgjmvcdLV7Mrd9TZjxNs8LmXVpqvANDf4FsSs5wLlzOykWLcaFzR3+5hdVEQ8ctmrUxgHlPA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-progressbar": {
|
"node_modules/electron-progressbar": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -24483,9 +24486,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-log": {
|
"electron-log": {
|
||||||
"version": "4.4.8",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.1.tgz",
|
||||||
"integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
|
"integrity": "sha512-x4wnwHg00h/onWQgjmvcdLV7Mrd9TZjxNs8LmXVpqvANDf4FsSs5wLlzOykWLcaFzR3+5hdVEQ8ctmrUxgHlPA=="
|
||||||
},
|
},
|
||||||
"electron-progressbar": {
|
"electron-progressbar": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.30.0",
|
"ace-builds": "^1.30.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^5.0.1",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
|||||||
6
src/application/Common/Log/Logger.ts
Normal file
6
src/application/Common/Log/Logger.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Logger {
|
||||||
|
info(...params: unknown[]): void;
|
||||||
|
warn(...params: unknown[]): void;
|
||||||
|
error(...params: unknown[]): void;
|
||||||
|
debug(...params: unknown[]): void;
|
||||||
|
}
|
||||||
5
src/application/Common/Log/LoggerFactory.ts
Normal file
5
src/application/Common/Log/LoggerFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
|
||||||
|
export interface LoggerFactory {
|
||||||
|
readonly logger: Logger;
|
||||||
|
}
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
import { ILogger } from './ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
|
||||||
export class ConsoleLogger implements ILogger {
|
export class ConsoleLogger implements Logger {
|
||||||
constructor(private readonly consoleProxy: Partial<Console> = console) {
|
constructor(private readonly consoleProxy: ConsoleLogFunctions = globalThis.console) {
|
||||||
if (!consoleProxy) { // do not trust strictNullChecks for global objects
|
if (!consoleProxy) { // do not trust strictNullChecks for global objects
|
||||||
throw new Error('missing console');
|
throw new Error('missing console');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public info(...params: unknown[]): void {
|
public info(...params: unknown[]): void {
|
||||||
const logFunction = this.consoleProxy?.info;
|
this.consoleProxy.info(...params);
|
||||||
if (!logFunction) {
|
}
|
||||||
throw new Error('missing "info" function');
|
|
||||||
}
|
public warn(...params: unknown[]): void {
|
||||||
logFunction.call(this.consoleProxy, ...params);
|
this.consoleProxy.warn(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(...params: unknown[]): void {
|
||||||
|
this.consoleProxy.error(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(...params: unknown[]): void {
|
||||||
|
this.consoleProxy.debug(...params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConsoleLogFunctions extends Partial<Console> {
|
||||||
|
readonly info: Console['info'];
|
||||||
|
readonly warn: Console['warn'];
|
||||||
|
readonly error: Console['error'];
|
||||||
|
readonly debug: Console['debug'];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { ElectronLog } from 'electron-log';
|
import log from 'electron-log/main';
|
||||||
import { ILogger } from './ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import type { LogFunctions } from 'electron-log';
|
||||||
|
|
||||||
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
||||||
export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
export function createElectronLogger(logger: LogFunctions = log): Logger {
|
||||||
if (!logger) {
|
|
||||||
throw new Error('missing logger');
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
info: (...params) => {
|
info: (...params) => logger.info(...params),
|
||||||
if (!logger.info) {
|
debug: (...params) => logger.debug(...params),
|
||||||
throw new Error('missing "info" function');
|
warn: (...params) => logger.warn(...params),
|
||||||
}
|
error: (...params) => logger.error(...params),
|
||||||
logger.info(...params);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ElectronLogger = createElectronLogger();
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface ILogger {
|
|
||||||
info (...params: unknown[]): void;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ILogger } from './ILogger';
|
|
||||||
|
|
||||||
export interface ILoggerFactory {
|
|
||||||
readonly logger: ILogger;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ILogger } from './ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
|
||||||
export class NoopLogger implements ILogger {
|
export class NoopLogger implements Logger {
|
||||||
public info(): void { /* NOOP */ }
|
public info(): void { /* NOOP */ }
|
||||||
|
|
||||||
|
public warn(): void { /* NOOP */ }
|
||||||
|
|
||||||
|
public error(): void { /* NOOP */ }
|
||||||
|
|
||||||
|
public debug(): void { /* NOOP */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||||
import { ILogger } from './ILogger';
|
|
||||||
|
|
||||||
export class WindowInjectedLogger implements ILogger {
|
export class WindowInjectedLogger implements Logger {
|
||||||
private readonly logger: ILogger;
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(windowVariables: WindowVariables | undefined | null = window) {
|
constructor(windowVariables: WindowVariables | undefined | null = window) {
|
||||||
if (!windowVariables) { // do not trust strict null checks for global objects
|
if (!windowVariables) { // do not trust strict null checks for global objects
|
||||||
@@ -17,4 +17,16 @@ export class WindowInjectedLogger implements ILogger {
|
|||||||
public info(...params: unknown[]): void {
|
public info(...params: unknown[]): void {
|
||||||
this.logger.info(...params);
|
this.logger.info(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public warn(...params: unknown[]): void {
|
||||||
|
this.logger.warn(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(...params: unknown[]): void {
|
||||||
|
this.logger.debug(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(...params: unknown[]): void {
|
||||||
|
this.logger.error(...params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
|
||||||
/* Primary entry point for platform-specific injections */
|
/* Primary entry point for platform-specific injections */
|
||||||
export interface WindowVariables {
|
export interface WindowVariables {
|
||||||
readonly isDesktop?: boolean;
|
readonly isDesktop: boolean;
|
||||||
readonly system?: ISystemOperations;
|
readonly system?: ISystemOperations;
|
||||||
readonly os?: OperatingSystem;
|
readonly os?: OperatingSystem;
|
||||||
readonly log?: ILogger;
|
readonly log: Logger;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||||
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { ILoggerFactory } from '@/infrastructure/Log/ILoggerFactory';
|
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||||
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||||
|
|
||||||
export class ClientLoggerFactory implements ILoggerFactory {
|
export class ClientLoggerFactory implements LoggerFactory {
|
||||||
public static readonly Current: ILoggerFactory = new ClientLoggerFactory();
|
public static readonly Current: LoggerFactory = new ClientLoggerFactory();
|
||||||
|
|
||||||
public readonly logger: ILogger;
|
public readonly logger: Logger;
|
||||||
|
|
||||||
protected constructor(environment: IRuntimeEnvironment = RuntimeEnvironment.CurrentEnvironment) {
|
protected constructor(environment: IRuntimeEnvironment = RuntimeEnvironment.CurrentEnvironment) {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@/presentation/injectionSymbols';
|
} from '@/presentation/injectionSymbols';
|
||||||
import { PropertyKeys } from '@/TypeHelpers';
|
import { PropertyKeys } from '@/TypeHelpers';
|
||||||
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
|
import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
|
||||||
|
|
||||||
export function provideDependencies(
|
export function provideDependencies(
|
||||||
context: IApplicationContext,
|
context: IApplicationContext,
|
||||||
@@ -57,6 +58,10 @@ export function provideDependencies(
|
|||||||
return useUserSelectionState(state, events);
|
return useUserSelectionState(state, events);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
useLogger: (di) => di.provide(
|
||||||
|
InjectionKeys.useLogger,
|
||||||
|
useLogger,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
registerAll(Object.values(resolvers), api);
|
registerAll(Object.values(resolvers), api);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { Bootstrapper } from '../Bootstrapper';
|
import { Bootstrapper } from '../Bootstrapper';
|
||||||
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||||
|
|
||||||
export class AppInitializationLogger implements Bootstrapper {
|
export class AppInitializationLogger implements Bootstrapper {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
private readonly logger: Logger = ClientLoggerFactory.Current.logger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async bootstrap(): Promise<void> {
|
public async bootstrap(): Promise<void> {
|
||||||
|
|||||||
@@ -17,16 +17,18 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import { dumpNames } from './DumpNames';
|
import { dumpNames } from './DumpNames';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
|
const { log } = injectKey((keys) => keys.useLogger);
|
||||||
const devActions: readonly DevAction[] = [
|
const devActions: readonly DevAction[] = [
|
||||||
{
|
{
|
||||||
name: 'Log script/category names',
|
name: 'Log script/category names',
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
const names = await dumpNames();
|
const names = await dumpNames();
|
||||||
console.log(names);
|
log.info(names);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
8
src/presentation/components/Shared/Hooks/UseLogger.ts
Normal file
8
src/presentation/components/Shared/Hooks/UseLogger.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||||
|
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
||||||
|
|
||||||
|
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||||
|
return {
|
||||||
|
log: factory.logger,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { app, dialog } from 'electron';
|
import { app, dialog } from 'electron';
|
||||||
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
||||||
import { ProgressInfo } from 'electron-builder';
|
import { ProgressInfo } from 'electron-builder';
|
||||||
import log from 'electron-log';
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||||
|
|
||||||
export async function handleAutoUpdate() {
|
export async function handleAutoUpdate() {
|
||||||
@@ -23,11 +23,11 @@ function startHandlingUpdateProgress() {
|
|||||||
On macOS, download-progress event is not called.
|
On macOS, download-progress event is not called.
|
||||||
So the indeterminate progress will continue until download is finished.
|
So the indeterminate progress will continue until download is finished.
|
||||||
*/
|
*/
|
||||||
log.debug('@download-progress@\n', progress);
|
ElectronLogger.debug('@download-progress@\n', progress);
|
||||||
progressBar.showProgress(progress);
|
progressBar.showProgress(progress);
|
||||||
});
|
});
|
||||||
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
|
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
|
||||||
log.info('@update-downloaded@\n', info);
|
ElectronLogger.info('@update-downloaded@\n', info);
|
||||||
progressBar.close();
|
progressBar.close();
|
||||||
await handleUpdateDownloaded();
|
await handleUpdateDownloaded();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { app, dialog, shell } from 'electron';
|
import { app, dialog, shell } from 'electron';
|
||||||
import { UpdateInfo } from 'electron-updater';
|
import { UpdateInfo } from 'electron-updater';
|
||||||
import log from 'electron-log';
|
|
||||||
import fetch from 'cross-fetch';
|
import fetch from 'cross-fetch';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||||
|
|
||||||
export function requiresManualUpdate(): boolean {
|
export function requiresManualUpdate(): boolean {
|
||||||
@@ -64,16 +64,16 @@ async function askForVisitingWebsiteForManualUpdate(): Promise<ManualDownloadDia
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function download(info: UpdateInfo, project: ProjectInformation) {
|
async function download(info: UpdateInfo, project: ProjectInformation) {
|
||||||
log.info('Downloading update manually');
|
ElectronLogger.info('Downloading update manually');
|
||||||
const progressBar = new UpdateProgressBar();
|
const progressBar = new UpdateProgressBar();
|
||||||
progressBar.showIndeterminateState();
|
progressBar.showIndeterminateState();
|
||||||
try {
|
try {
|
||||||
const filePath = `${path.dirname(app.getPath('temp'))}/privacy.sexy/${info.version}-installer.dmg`;
|
const filePath = `${path.dirname(app.getPath('temp'))}/privacy.sexy/${info.version}-installer.dmg`;
|
||||||
const parentFolder = path.dirname(filePath);
|
const parentFolder = path.dirname(filePath);
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
log.info('Update is already downloaded');
|
ElectronLogger.info('Update is already downloaded');
|
||||||
await fs.promises.unlink(filePath);
|
await fs.promises.unlink(filePath);
|
||||||
log.info(`Deleted ${filePath}`);
|
ElectronLogger.info(`Deleted ${filePath}`);
|
||||||
} else {
|
} else {
|
||||||
await fs.promises.mkdir(parentFolder, { recursive: true });
|
await fs.promises.mkdir(parentFolder, { recursive: true });
|
||||||
}
|
}
|
||||||
@@ -99,20 +99,20 @@ async function downloadFileWithProgress(
|
|||||||
progressHandler: ProgressCallback,
|
progressHandler: ProgressCallback,
|
||||||
) {
|
) {
|
||||||
// We don't download through autoUpdater as it cannot download DMG but requires distributing ZIP
|
// We don't download through autoUpdater as it cannot download DMG but requires distributing ZIP
|
||||||
log.info(`Fetching ${url}`);
|
ElectronLogger.info(`Fetching ${url}`);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw Error(`Unable to download, server returned ${response.status} ${response.statusText}`);
|
throw Error(`Unable to download, server returned ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const contentLengthString = response.headers.get('content-length');
|
const contentLengthString = response.headers.get('content-length');
|
||||||
if (contentLengthString === null || contentLengthString === undefined) {
|
if (contentLengthString === null || contentLengthString === undefined) {
|
||||||
log.error('Content-Length header is missing');
|
ElectronLogger.error('Content-Length header is missing');
|
||||||
}
|
}
|
||||||
const contentLength = +(contentLengthString ?? 0);
|
const contentLength = +(contentLengthString ?? 0);
|
||||||
const writer = fs.createWriteStream(filePath);
|
const writer = fs.createWriteStream(filePath);
|
||||||
log.info(`Writing to ${filePath}, content length: ${contentLength}`);
|
ElectronLogger.info(`Writing to ${filePath}, content length: ${contentLength}`);
|
||||||
if (Number.isNaN(contentLength) || contentLength <= 0) {
|
if (Number.isNaN(contentLength) || contentLength <= 0) {
|
||||||
log.error('Unknown content-length', Array.from(response.headers.entries()));
|
ElectronLogger.error('Unknown content-length', Array.from(response.headers.entries()));
|
||||||
progressHandler = () => { /* do nothing */ };
|
progressHandler = () => { /* do nothing */ };
|
||||||
}
|
}
|
||||||
const reader = getReader(response);
|
const reader = getReader(response);
|
||||||
@@ -137,9 +137,9 @@ async function streamWithProgress(
|
|||||||
receivedLength += chunk.length;
|
receivedLength += chunk.length;
|
||||||
const percentage = Math.floor((receivedLength / totalLength) * 100);
|
const percentage = Math.floor((receivedLength / totalLength) * 100);
|
||||||
progressHandler(percentage);
|
progressHandler(percentage);
|
||||||
log.debug(`Received ${receivedLength} of ${totalLength}`);
|
ElectronLogger.debug(`Received ${receivedLength} of ${totalLength}`);
|
||||||
}
|
}
|
||||||
log.info('Downloaded successfully');
|
ElectronLogger.info('Downloaded successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReader(response: Response): NodeJS.ReadableStream | undefined {
|
function getReader(response: Response): NodeJS.ReadableStream | undefined {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import ProgressBar from 'electron-progressbar';
|
import ProgressBar from 'electron-progressbar';
|
||||||
import { ProgressInfo } from 'electron-builder';
|
import { ProgressInfo } from 'electron-builder';
|
||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import log from 'electron-log';
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
|
||||||
export class UpdateProgressBar {
|
export class UpdateProgressBar {
|
||||||
private progressBar: ProgressBar | undefined;
|
private progressBar: ProgressBar | undefined;
|
||||||
@@ -81,7 +81,7 @@ const progressBarFactory = {
|
|||||||
progressBar.detail = 'Download completed.';
|
progressBar.detail = 'Download completed.';
|
||||||
})
|
})
|
||||||
.on('aborted', (value: number) => {
|
.on('aborted', (value: number) => {
|
||||||
log.info(`progress aborted... ${value}`);
|
ElectronLogger.info(`Progress aborted... ${value}`);
|
||||||
})
|
})
|
||||||
.on('progress', (value: number) => {
|
.on('progress', (value: number) => {
|
||||||
progressBar.detail = `${value}% ...`;
|
progressBar.detail = `${value}% ...`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
||||||
import log from 'electron-log';
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { handleManualUpdate, requiresManualUpdate } from './ManualUpdater';
|
import { handleManualUpdate, requiresManualUpdate } from './ManualUpdater';
|
||||||
import { handleAutoUpdate } from './AutoUpdater';
|
import { handleAutoUpdate } from './AutoUpdater';
|
||||||
|
|
||||||
@@ -8,21 +8,21 @@ interface IUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setupAutoUpdater(): IUpdater {
|
export function setupAutoUpdater(): IUpdater {
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = ElectronLogger;
|
||||||
|
|
||||||
// Disable autodownloads because "checking" and "downloading" are handled separately based on the
|
// Disable autodownloads because "checking" and "downloading" are handled separately based on the
|
||||||
// current platform and user's choice.
|
// current platform and user's choice.
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
|
|
||||||
autoUpdater.on('error', (error: Error) => {
|
autoUpdater.on('error', (error: Error) => {
|
||||||
log.error('@error@\n', error);
|
ElectronLogger.error('@error@\n', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
let isAlreadyHandled = false;
|
let isAlreadyHandled = false;
|
||||||
autoUpdater.on('update-available', async (info: UpdateInfo) => {
|
autoUpdater.on('update-available', async (info: UpdateInfo) => {
|
||||||
log.info('@update-available@\n', info);
|
ElectronLogger.info('@update-available@\n', info);
|
||||||
if (isAlreadyHandled) {
|
if (isAlreadyHandled) {
|
||||||
log.info('Available updates is already handled');
|
ElectronLogger.info('Available updates is already handled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isAlreadyHandled = true;
|
isAlreadyHandled = true;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
import {
|
import {
|
||||||
app, protocol, BrowserWindow, shell, screen,
|
app, protocol, BrowserWindow, shell, screen,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
import log from 'electron-log/main';
|
||||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
import log from 'electron-log';
|
|
||||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { setupAutoUpdater } from './Update/Updater';
|
import { setupAutoUpdater } from './Update/Updater';
|
||||||
import {
|
import {
|
||||||
APP_ICON_PATH, PRELOADER_SCRIPT_PATH, RENDERER_HTML_PATH, RENDERER_URL,
|
APP_ICON_PATH, PRELOADER_SCRIPT_PATH, RENDERER_HTML_PATH, RENDERER_URL,
|
||||||
@@ -23,6 +24,7 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
setupLogger();
|
setupLogger();
|
||||||
|
|
||||||
validateRuntimeSanity({
|
validateRuntimeSanity({
|
||||||
// Metadata is used by manual updates.
|
// Metadata is used by manual updates.
|
||||||
validateEnvironmentVariables: true,
|
validateEnvironmentVariables: true,
|
||||||
@@ -89,7 +91,7 @@ app.on('ready', async () => {
|
|||||||
try {
|
try {
|
||||||
await installExtension(VUEJS_DEVTOOLS);
|
await installExtension(VUEJS_DEVTOOLS);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('Vue Devtools failed to install:', e.toString());
|
ElectronLogger.error('Vue Devtools failed to install:', e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -123,7 +125,7 @@ function loadApplication(window: BrowserWindow) {
|
|||||||
updater.checkForUpdates();
|
updater.checkForUpdates();
|
||||||
}
|
}
|
||||||
// Do not remove [WINDOW_INIT]; it's a marker used in tests.
|
// Do not remove [WINDOW_INIT]; it's a marker used in tests.
|
||||||
log.info('[WINDOW_INIT] Main window initialized and content loading.');
|
ElectronLogger.info('[WINDOW_INIT] Main window initialized and content loading.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
||||||
@@ -150,5 +152,7 @@ function getWindowSize(idealWidth: number, idealHeight: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupLogger(): void {
|
function setupLogger(): void {
|
||||||
|
// log.initialize(); ← We inject logger to renderer through preloader, this is not needed.
|
||||||
log.transports.file.level = 'silly';
|
log.transports.file.level = 'silly';
|
||||||
|
log.eventLogger.startLogging();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import log from 'electron-log';
|
|
||||||
import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations';
|
import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations';
|
||||||
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { convertPlatformToOs } from './NodeOsMapper';
|
import { convertPlatformToOs } from './NodeOsMapper';
|
||||||
|
|
||||||
export function provideWindowVariables(
|
export function provideWindowVariables(
|
||||||
createSystem = createNodeSystemOperations,
|
createSystem = createNodeSystemOperations,
|
||||||
createLogger: () => ILogger = () => createElectronLogger(log),
|
createLogger: () => Logger = () => createElectronLogger(),
|
||||||
convertToOs = convertPlatformToOs,
|
convertToOs = convertPlatformToOs,
|
||||||
): WindowVariables {
|
): WindowVariables {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// This file is used to securely expose Electron APIs to the application.
|
// This file is used to securely expose Electron APIs to the application.
|
||||||
|
|
||||||
import { contextBridge } from 'electron';
|
import { contextBridge } from 'electron';
|
||||||
import log from 'electron-log';
|
|
||||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { provideWindowVariables } from './WindowVariablesProvider';
|
import { provideWindowVariables } from './WindowVariablesProvider';
|
||||||
|
|
||||||
validateRuntimeSanity({
|
validateRuntimeSanity({
|
||||||
@@ -20,4 +20,4 @@ Object.entries(windowVariables).forEach(([key, value]) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
|
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
|
||||||
log.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
|
ElectronLogger.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipbo
|
|||||||
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||||
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||||
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
|
import type { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
|
||||||
|
|
||||||
export const InjectionKeys = {
|
export const InjectionKeys = {
|
||||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||||
@@ -15,6 +16,7 @@ export const InjectionKeys = {
|
|||||||
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
||||||
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
||||||
useUserSelectionState: defineTransientKey<ReturnType<typeof useUserSelectionState>>('useUserSelectionState'),
|
useUserSelectionState: defineTransientKey<ReturnType<typeof useUserSelectionState>>('useUserSelectionState'),
|
||||||
|
useLogger: defineTransientKey<ReturnType<typeof useLogger>>('useLogger'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InjectionKeyWithLifetime<T> {
|
export interface InjectionKeyWithLifetime<T> {
|
||||||
|
|||||||
@@ -5,33 +5,28 @@ import { exists } from '../utils/io';
|
|||||||
import { SupportedPlatform, CURRENT_PLATFORM } from '../utils/platform';
|
import { SupportedPlatform, CURRENT_PLATFORM } from '../utils/platform';
|
||||||
import { getAppName } from '../utils/npm';
|
import { getAppName } from '../utils/npm';
|
||||||
|
|
||||||
const LOG_FILE_NAMES = ['main', 'renderer'];
|
|
||||||
|
|
||||||
export async function clearAppLogFiles(
|
export async function clearAppLogFiles(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
await Promise.all(LOG_FILE_NAMES.map(async (logFileName) => {
|
const logPath = await determineLogPath(projectDir);
|
||||||
const logPath = await determineLogPath(projectDir, logFileName);
|
if (!logPath || !await exists(logPath)) {
|
||||||
if (!logPath || !await exists(logPath)) {
|
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
||||||
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
try {
|
||||||
try {
|
await unlink(logPath);
|
||||||
await unlink(logPath);
|
log(`Successfully cleared the log file at: ${logPath}.`);
|
||||||
log(`Successfully cleared the log file at: ${logPath}.`);
|
} catch (error) {
|
||||||
} catch (error) {
|
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
||||||
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
}
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readAppLogFile(
|
export async function readAppLogFile(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
logFileName: string,
|
|
||||||
): Promise<AppLogFileResult> {
|
): Promise<AppLogFileResult> {
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
const logPath = await determineLogPath(projectDir, logFileName);
|
const logPath = await determineLogPath(projectDir);
|
||||||
if (!logPath || !await exists(logPath)) {
|
if (!logPath || !await exists(logPath)) {
|
||||||
log(`No log file at: ${logPath}`, LogLevel.Warn);
|
log(`No log file at: ${logPath}`, LogLevel.Warn);
|
||||||
return {
|
return {
|
||||||
@@ -52,10 +47,9 @@ interface AppLogFileResult {
|
|||||||
|
|
||||||
async function determineLogPath(
|
async function determineLogPath(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
logFileName: string,
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
if (!LOG_FILE_NAMES.includes(logFileName)) { throw new Error(`unknown log file name: ${logFileName}`); }
|
const logFileName = 'main.log';
|
||||||
const appName = await getAppName(projectDir);
|
const appName = await getAppName(projectDir);
|
||||||
if (!appName) {
|
if (!appName) {
|
||||||
return die('App name not found.');
|
return die('App name not found.');
|
||||||
@@ -67,19 +61,19 @@ async function determineLogPath(
|
|||||||
if (!process.env.HOME) {
|
if (!process.env.HOME) {
|
||||||
throw new Error('HOME environment variable is not defined');
|
throw new Error('HOME environment variable is not defined');
|
||||||
}
|
}
|
||||||
return join(process.env.HOME, 'Library', 'Logs', appName, `${logFileName}.log`);
|
return join(process.env.HOME, 'Library', 'Logs', appName, logFileName);
|
||||||
},
|
},
|
||||||
[SupportedPlatform.Linux]: () => {
|
[SupportedPlatform.Linux]: () => {
|
||||||
if (!process.env.HOME) {
|
if (!process.env.HOME) {
|
||||||
throw new Error('HOME environment variable is not defined');
|
throw new Error('HOME environment variable is not defined');
|
||||||
}
|
}
|
||||||
return join(process.env.HOME, '.config', appName, 'logs', `${logFileName}.log`);
|
return join(process.env.HOME, '.config', appName, 'logs', logFileName);
|
||||||
},
|
},
|
||||||
[SupportedPlatform.Windows]: () => {
|
[SupportedPlatform.Windows]: () => {
|
||||||
if (!process.env.USERPROFILE) {
|
if (!process.env.USERPROFILE) {
|
||||||
throw new Error('USERPROFILE environment variable is not defined');
|
throw new Error('USERPROFILE environment variable is not defined');
|
||||||
}
|
}
|
||||||
return join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', `${logFileName}.log`);
|
return join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', logFileName);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { splitTextIntoLines, indentText, filterEmpty } from '../utils/text';
|
import { splitTextIntoLines, indentText } from '../utils/text';
|
||||||
import { log, die } from '../utils/log';
|
import { log, die } from '../utils/log';
|
||||||
import { readAppLogFile } from './app-logs';
|
import { readAppLogFile } from './app-logs';
|
||||||
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
||||||
@@ -11,8 +11,6 @@ const EXPECTED_LOG_MARKERS = [
|
|||||||
'[APP_INIT]',
|
'[APP_INIT]',
|
||||||
];
|
];
|
||||||
|
|
||||||
type ProcessType = 'main' | 'renderer';
|
|
||||||
|
|
||||||
export async function checkForErrors(
|
export async function checkForErrors(
|
||||||
stderr: string,
|
stderr: string,
|
||||||
windowTitles: readonly string[],
|
windowTitles: readonly string[],
|
||||||
@@ -31,13 +29,11 @@ async function gatherErrors(
|
|||||||
projectDir: string,
|
projectDir: string,
|
||||||
): Promise<ExecutionError[]> {
|
): Promise<ExecutionError[]> {
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir, 'main');
|
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir);
|
||||||
const { logFileContent: rendererLogs, logFilePath: rendererLogFile } = await readAppLogFile(projectDir, 'renderer');
|
const allLogs = [mainLogs, stderr].filter(Boolean).join('\n');
|
||||||
const allLogs = filterEmpty([mainLogs, rendererLogs, stderr]).join('\n');
|
|
||||||
return [
|
return [
|
||||||
verifyStdErr(stderr),
|
verifyStdErr(stderr),
|
||||||
verifyApplicationLogsExist('main', mainLogs, mainLogFile),
|
verifyApplicationLogsExist(mainLogs, mainLogFile),
|
||||||
verifyApplicationLogsExist('renderer', rendererLogs, rendererLogFile),
|
|
||||||
...EXPECTED_LOG_MARKERS.map(
|
...EXPECTED_LOG_MARKERS.map(
|
||||||
(marker) => verifyLogMarkerExistsInLogs(allLogs, marker),
|
(marker) => verifyLogMarkerExistsInLogs(allLogs, marker),
|
||||||
),
|
),
|
||||||
@@ -72,13 +68,12 @@ function formatError(error: ExecutionError): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function verifyApplicationLogsExist(
|
function verifyApplicationLogsExist(
|
||||||
processType: ProcessType,
|
|
||||||
logContent: string | undefined,
|
logContent: string | undefined,
|
||||||
logFilePath: string,
|
logFilePath: string,
|
||||||
): ExecutionError | undefined {
|
): ExecutionError | undefined {
|
||||||
if (!logContent?.length) {
|
if (!logContent?.length) {
|
||||||
return describeError(
|
return describeError(
|
||||||
`Missing application (${processType}) logs`,
|
'Missing application logs',
|
||||||
'Application logs are empty not were not found.'
|
'Application logs are empty not were not found.'
|
||||||
+ `\nLog path: ${logFilePath}`,
|
+ `\nLog path: ${logFilePath}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,21 +32,6 @@ describe('ConsoleLogger', () => {
|
|||||||
expect(consoleMock.callHistory[0].args).to.deep.equal(expectedParams);
|
expect(consoleMock.callHistory[0].args).to.deep.equal(expectedParams);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('throws if log function is missing', () => {
|
|
||||||
itEachLoggingMethod((functionName, testParameters) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = `missing "${functionName}" function`;
|
|
||||||
const consoleMock = {} as Partial<Console>;
|
|
||||||
consoleMock[functionName] = undefined;
|
|
||||||
const logger = new ConsoleLogger(consoleMock);
|
|
||||||
|
|
||||||
// act
|
|
||||||
const act = () => logger[functionName](...testParameters);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class MockConsole
|
class MockConsole
|
||||||
@@ -58,4 +43,25 @@ class MockConsole
|
|||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public warn(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'warn',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'debug',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'error',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,15 @@
|
|||||||
import { describe, expect } from 'vitest';
|
import { describe, expect } from 'vitest';
|
||||||
import { ElectronLog } from 'electron-log';
|
|
||||||
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
|
||||||
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
import { itEachLoggingMethod } from './LoggerTestRunner';
|
import { itEachLoggingMethod } from './LoggerTestRunner';
|
||||||
|
import type { LogFunctions } from 'electron-log';
|
||||||
|
|
||||||
describe('ElectronLogger', () => {
|
describe('ElectronLogger', () => {
|
||||||
describe('throws if logger is missing', () => {
|
|
||||||
itEachAbsentObjectValue((absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'missing logger';
|
|
||||||
const electronLog = absentValue as never;
|
|
||||||
// act
|
|
||||||
const act = () => createElectronLogger(electronLog);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
}, { excludeUndefined: true });
|
|
||||||
});
|
|
||||||
describe('throws if log function is missing', () => {
|
|
||||||
itEachLoggingMethod((functionName, testParameters) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = `missing "${functionName}" function`;
|
|
||||||
const electronLogMock = {} as Partial<ElectronLog>;
|
|
||||||
electronLogMock[functionName] = undefined;
|
|
||||||
const logger = createElectronLogger(electronLogMock);
|
|
||||||
|
|
||||||
// act
|
|
||||||
const act = () => logger[functionName](...testParameters);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('methods log the provided params', () => {
|
describe('methods log the provided params', () => {
|
||||||
itEachLoggingMethod((functionName, testParameters) => {
|
itEachLoggingMethod((functionName, testParameters) => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedParams = testParameters;
|
const expectedParams = testParameters;
|
||||||
const electronLogMock = new MockElectronLog();
|
const electronLogMock = new ElectronLogStub();
|
||||||
const logger = createElectronLogger(electronLogMock);
|
const logger = createElectronLogger(electronLogMock);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
@@ -50,9 +23,51 @@ describe('ElectronLogger', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class MockElectronLog
|
class ElectronLogStub
|
||||||
extends StubWithObservableMethodCalls<ElectronLog>
|
extends StubWithObservableMethodCalls<LogFunctions>
|
||||||
implements Partial<ElectronLog> {
|
implements LogFunctions {
|
||||||
|
public error(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'error',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public warn(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'warn',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public verbose(...args: unknown[]): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'verbose',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'debug',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public silly(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'silly',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(...args: unknown[]) {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'log',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public info(...args: unknown[]) {
|
public info(...args: unknown[]) {
|
||||||
this.registerMethodCall({
|
this.registerMethodCall({
|
||||||
methodName: 'info',
|
methodName: 'info',
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { it } from 'vitest';
|
import { it } from 'vitest';
|
||||||
import { FunctionKeys } from '@/TypeHelpers';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
|
||||||
|
|
||||||
type TestParameters = [string, number, { some: string }];
|
|
||||||
|
|
||||||
export function itEachLoggingMethod(
|
export function itEachLoggingMethod(
|
||||||
handler: (
|
handler: (
|
||||||
functionName: keyof ILogger,
|
functionName: keyof Logger,
|
||||||
testParameters: TestParameters,
|
testParameters: readonly unknown[]
|
||||||
) => void,
|
) => void,
|
||||||
) {
|
) {
|
||||||
const testParameters: TestParameters = ['test', 123, { some: 'object' }];
|
const testScenarios: {
|
||||||
const loggerMethods: Array<FunctionKeys<ILogger>> = [
|
readonly [FunctionName in keyof Logger]: Parameters<Logger[FunctionName]>;
|
||||||
'info',
|
} = {
|
||||||
];
|
info: ['single-string'],
|
||||||
loggerMethods
|
warn: ['with number', 123],
|
||||||
.forEach((functionKey) => {
|
debug: ['with simple object', { some: 'object' }],
|
||||||
|
error: ['with error object', new Error('error')],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(testScenarios)
|
||||||
|
.forEach(([functionKey, testParameters]) => {
|
||||||
it(functionKey, () => {
|
it(functionKey, () => {
|
||||||
handler(functionKey, testParameters);
|
handler(functionKey as keyof Logger, testParameters);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect } from 'vitest';
|
import { describe, expect } from 'vitest';
|
||||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { itEachLoggingMethod } from './LoggerTestRunner';
|
import { itEachLoggingMethod } from './LoggerTestRunner';
|
||||||
|
|
||||||
describe('NoopLogger', () => {
|
describe('NoopLogger', () => {
|
||||||
@@ -8,7 +8,7 @@ describe('NoopLogger', () => {
|
|||||||
itEachLoggingMethod((functionName, testParameters) => {
|
itEachLoggingMethod((functionName, testParameters) => {
|
||||||
// arrange
|
// arrange
|
||||||
const randomParams = testParameters;
|
const randomParams = testParameters;
|
||||||
const logger: ILogger = new NoopLogger();
|
const logger: Logger = new NoopLogger();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const act = () => logger[functionName](...randomParams);
|
const act = () => logger[functionName](...randomParams);
|
||||||
|
|||||||
@@ -177,10 +177,11 @@ function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
|
|||||||
describe('does not object type when not on desktop', () => {
|
describe('does not object type when not on desktop', () => {
|
||||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const isOnDesktop = false;
|
||||||
const invalidObject = invalidObjectValue as T;
|
const invalidObject = invalidObjectValue as T;
|
||||||
const input: WindowVariables = {
|
const input: WindowVariables = {
|
||||||
...new WindowVariablesStub(),
|
...new WindowVariablesStub(),
|
||||||
isDesktop: undefined,
|
isDesktop: isOnDesktop,
|
||||||
[key]: invalidObject,
|
[key]: invalidObject,
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||||
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||||
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||||
@@ -29,7 +29,7 @@ describe('ClientLoggerFactory', () => {
|
|||||||
});
|
});
|
||||||
const testCases: Array<{
|
const testCases: Array<{
|
||||||
readonly description: string,
|
readonly description: string,
|
||||||
readonly expectedType: Constructible<ILogger>,
|
readonly expectedType: Constructible<Logger>,
|
||||||
readonly environment: IRuntimeEnvironment,
|
readonly environment: IRuntimeEnvironment,
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('DependencyProvider', () => {
|
|||||||
useClipboard: createTransientTests(),
|
useClipboard: createTransientTests(),
|
||||||
useCurrentCode: createTransientTests(),
|
useCurrentCode: createTransientTests(),
|
||||||
useUserSelectionState: createTransientTests(),
|
useUserSelectionState: createTransientTests(),
|
||||||
|
useLogger: createTransientTests(),
|
||||||
};
|
};
|
||||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||||
const registeredKey = InjectionKeys[key].key;
|
const registeredKey = InjectionKeys[key].key;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
|
||||||
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||||
|
import { LoggerFactoryStub } from '@tests/unit/shared/Stubs/LoggerFactoryStub';
|
||||||
|
|
||||||
|
describe('UseLogger', () => {
|
||||||
|
it('returns expected logger from factory', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedLogger = new LoggerStub();
|
||||||
|
const factory = new LoggerFactoryStub()
|
||||||
|
.withLogger(expectedLogger);
|
||||||
|
// act
|
||||||
|
const { log: actualLogger } = useLogger(factory);
|
||||||
|
// assert
|
||||||
|
expect(actualLogger).to.equal(expectedLogger);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { provideWindowVariables } from '@/presentation/electron/preload/WindowVa
|
|||||||
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||||
|
|
||||||
describe('WindowVariablesProvider', () => {
|
describe('WindowVariablesProvider', () => {
|
||||||
@@ -55,7 +55,7 @@ class TestContext {
|
|||||||
|
|
||||||
private os: OperatingSystem = OperatingSystem.Android;
|
private os: OperatingSystem = OperatingSystem.Android;
|
||||||
|
|
||||||
private log: ILogger = new LoggerStub();
|
private log: Logger = new LoggerStub();
|
||||||
|
|
||||||
public withSystem(system: ISystemOperations): this {
|
public withSystem(system: ISystemOperations): this {
|
||||||
this.system = system;
|
this.system = system;
|
||||||
@@ -67,7 +67,7 @@ class TestContext {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withLogger(log: ILogger): this {
|
public withLogger(log: Logger): this {
|
||||||
this.log = log;
|
this.log = log;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
12
tests/unit/shared/Stubs/LoggerFactoryStub.ts
Normal file
12
tests/unit/shared/Stubs/LoggerFactoryStub.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||||
|
import { LoggerStub } from './LoggerStub';
|
||||||
|
|
||||||
|
export class LoggerFactoryStub implements LoggerFactory {
|
||||||
|
public logger: Logger = new LoggerStub();
|
||||||
|
|
||||||
|
public withLogger(logger: Logger): this {
|
||||||
|
this.logger = logger;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,32 @@
|
|||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
export class LoggerStub extends StubWithObservableMethodCalls<ILogger> implements ILogger {
|
export class LoggerStub extends StubWithObservableMethodCalls<Logger> implements Logger {
|
||||||
|
public warn(...params: unknown[]): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'warn',
|
||||||
|
args: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(...params: unknown[]): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'error',
|
||||||
|
args: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(...params: unknown[]): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'debug',
|
||||||
|
args: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public info(...params: unknown[]): void {
|
public info(...params: unknown[]): void {
|
||||||
this.registerMethodCall({
|
this.registerMethodCall({
|
||||||
methodName: 'info',
|
methodName: 'info',
|
||||||
args: params,
|
args: params,
|
||||||
});
|
});
|
||||||
console.log(...params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { SystemOperationsStub } from './SystemOperationsStub';
|
import { SystemOperationsStub } from './SystemOperationsStub';
|
||||||
@@ -8,18 +8,18 @@ import { LoggerStub } from './LoggerStub';
|
|||||||
export class WindowVariablesStub implements WindowVariables {
|
export class WindowVariablesStub implements WindowVariables {
|
||||||
public system?: ISystemOperations = new SystemOperationsStub();
|
public system?: ISystemOperations = new SystemOperationsStub();
|
||||||
|
|
||||||
public isDesktop? = false;
|
public isDesktop = false;
|
||||||
|
|
||||||
public os?: OperatingSystem = OperatingSystem.BlackBerryOS;
|
public os?: OperatingSystem = OperatingSystem.BlackBerryOS;
|
||||||
|
|
||||||
public log?: ILogger = new LoggerStub();
|
public log: Logger = new LoggerStub();
|
||||||
|
|
||||||
public withLog(log?: ILogger): this {
|
public withLog(log: Logger): this {
|
||||||
this.log = log;
|
this.log = log;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withIsDesktop(value?: boolean): this {
|
public withIsDesktop(value: boolean): this {
|
||||||
this.isDesktop = value;
|
this.isDesktop = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user