diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-01-15 13:33:42 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-01-15 13:33:42 +0100 |
| commit | 40a405734f1e132455d315365033001422f9aeb2 (patch) | |
| tree | 6a4680d9ad599861246053404211ec8b6d5675c5 /gui | |
| parent | 6b5d1c402d56243512d8dec1a13ff8a2439ad5ef (diff) | |
| parent | ef332247aa3408b2af862340cf3fe1ae4ee5054a (diff) | |
| download | mullvadvpn-40a405734f1e132455d315365033001422f9aeb2.tar.xz mullvadvpn-40a405734f1e132455d315365033001422f9aeb2.zip | |
Merge branch 'replace-electron-log'
Diffstat (limited to 'gui')
31 files changed, 249 insertions, 142 deletions
diff --git a/gui/package-lock.json b/gui/package-lock.json index 5f39928fa0..5ef62841a5 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -4224,11 +4224,6 @@ } } }, - "electron-log": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.1.1.tgz", - "integrity": "sha512-vkK3rNBOciRiinxrsdgg8hyUia+ct8ZMjBblvKjgNk4uHEDjjSyn313NOwv75xOMVIKlfmYzxaN8kR/oGC33aQ==" - }, "electron-mocha": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/electron-mocha/-/electron-mocha-9.3.3.tgz", @@ -8698,11 +8693,6 @@ } } }, - "mkdirp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", - "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" - }, "mocha": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", diff --git a/gui/package.json b/gui/package.json index 23802f2494..0bb993f9df 100644 --- a/gui/package.json +++ b/gui/package.json @@ -15,12 +15,10 @@ "@grpc/grpc-js": "^1.1.2", "argv-split": "^2.0.1", "d3-geo": "^1.12.1", - "electron-log": "^4.1.1", "gettext-parser": "^4.0.3", "google-protobuf": "^4.0.0-rc.2", "ip": "^1.1.5", "linux-app-list": "^1.0.1", - "mkdirp": "^1.0.3", "moment": "^2.24.0", "node-gettext": "^3.0.0", "rbush": "^2.0.2", @@ -103,7 +101,7 @@ "lint": "eslint --ext tsx,ts .", "format": "prettier \"**/*.{js,css,ts,tsx}\" --write", "develop": "gulp develop", - "test": "cross-env NODE_ENV=test electron-mocha --renderer --reporter spec --require-main \"test/setup/main.js\" --require ts-node/register --require \"test/setup/renderer.ts\" \"test/**/*.{ts,tsx}\"", + "test": "cross-env NODE_ENV=test electron-mocha --renderer --reporter spec --require ts-node/register --require \"test/setup/renderer.ts\" \"test/**/*.{ts,tsx}\"", "update-translations": "node scripts/extract-translations", "pack:mac": "gulp pack-mac", "pack:win": "gulp pack-win", diff --git a/gui/src/main/account-data-cache.ts b/gui/src/main/account-data-cache.ts index 5c178e83a6..a03728e004 100644 --- a/gui/src/main/account-data-cache.ts +++ b/gui/src/main/account-data-cache.ts @@ -1,7 +1,7 @@ -import log from 'electron-log'; import moment from 'moment'; import { hasExpired } from '../shared/account-expiry'; import { AccountToken, IAccountData } from '../shared/daemon-rpc-types'; +import log from '../shared/logging'; import consumePromise from '../shared/promise'; import { Scheduler } from '../shared/scheduler'; import { InvalidAccountError } from './errors'; diff --git a/gui/src/main/autostart.ts b/gui/src/main/autostart.ts index a42691550c..f5369b6723 100644 --- a/gui/src/main/autostart.ts +++ b/gui/src/main/autostart.ts @@ -1,8 +1,8 @@ import { app } from 'electron'; -import log from 'electron-log'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; +import log from '../shared/logging'; const DESKTOP_FILE_NAME = 'mullvad-vpn.desktop'; diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 7f9e6d629b..9be0fc6870 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -4,7 +4,6 @@ import { StringValue, UInt32Value, } from 'google-protobuf/google/protobuf/wrappers_pb.js'; -import log from 'electron-log'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb.js'; import { promisify } from 'util'; import { @@ -50,6 +49,7 @@ import { TunnelProtocol, IDnsOptions, } from '../shared/daemon-rpc-types'; +import log from '../shared/logging'; import * as managementInterface from './management_interface/management_interface_grpc_pb'; import * as grpcTypes from './management_interface/management_interface_pb'; diff --git a/gui/src/main/gui-settings.ts b/gui/src/main/gui-settings.ts index e7d8a47cf1..54b51116cd 100644 --- a/gui/src/main/gui-settings.ts +++ b/gui/src/main/gui-settings.ts @@ -1,8 +1,8 @@ import { app } from 'electron'; -import log from 'electron-log'; import * as fs from 'fs'; import * as path from 'path'; import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; +import log from '../shared/logging'; const settingsSchema = { preferredLocale: 'string', diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 535d893bb8..402a0a35cb 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -10,8 +10,6 @@ import { shell, Tray, } from 'electron'; -import log from 'electron-log'; -import mkdirp from 'mkdirp'; import moment from 'moment'; import * as path from 'path'; import { sprintf } from 'sprintf-js'; @@ -39,14 +37,8 @@ import { import { loadTranslations, messages } from '../shared/gettext'; import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; import { IpcMainEventChannel } from '../shared/ipc-event-channel'; -import { - backupLogFile, - cleanUpLogDirectory, - getLogsDirectory, - getMainLogFile, - getRendererLogFile, - setupLogging, -} from '../shared/logging'; +import log, { ConsoleOutput, Logger } from '../shared/logging'; +import { LogLevel } from '../shared/logging-types'; import { AccountExpiredNotificationProvider, CloseToAccountExpiryNotificationProvider, @@ -63,6 +55,16 @@ import { InvalidAccountError } from './errors'; import Expectation from './expectation'; import GuiSettings from './gui-settings'; import { getAppIcon } from './linux-desktop-entry'; +import { + backupLogFile, + cleanUpLogDirectory, + createLoggingDirectory, + FileOutput, + getMainLogPath, + getRendererLogPath, + IpcInput, + OLD_LOG_FILES, +} from './logging'; import NotificationController from './notification-controller'; import { resolveBin } from './proc'; import ReconnectionBackoff from './reconnection-backoff'; @@ -201,6 +203,8 @@ class ApplicationMain { private autoConnectOnWireguardKeyEvent = false; private autoConnectFallbackScheduler = new Scheduler(); + private rendererLog?: Logger; + public run() { // Remove window animations to combat window flickering when opening window. Can be removed when // this issue has been resolved: https://github.com/electron/electron/issues/12130 @@ -252,28 +256,35 @@ class ApplicationMain { if (appDataDir) { app.setPath('appData', appDataDir); app.setPath('userData', path.join(appDataDir, app.name)); + app.setPath('logs', path.join(appDataDir, app.name, 'logs')); } else { throw new Error('Missing %LOCALAPPDATA% environment variable'); } + } else if (process.platform === 'linux') { + const userDataDir = app.getPath('userData'); + app.setPath('logs', path.join(userDataDir, 'logs')); } } private initLogging() { - const logDirectory = getLogsDirectory(); - const mainLogFile = getMainLogFile(); - const logFiles = [mainLogFile, getRendererLogFile()]; + const mainLogPath = getMainLogPath(); + const rendererLogPath = getRendererLogPath(); if (process.env.NODE_ENV !== 'development') { - // Ensure log directory exists - mkdirp.sync(logDirectory); + createLoggingDirectory(); + cleanUpLogDirectory(OLD_LOG_FILES); - for (const logFile of logFiles) { - backupLogFile(logFile); - } + backupLogFile(mainLogPath); + backupLogFile(rendererLogPath); + + log.addOutput(new FileOutput(LogLevel.debug, mainLogPath)); + + this.rendererLog = new Logger(); + this.rendererLog.addInput(new IpcInput()); + this.rendererLog.addOutput(new FileOutput(LogLevel.debug, rendererLogPath)); } - cleanUpLogDirectory(); - setupLogging(mainLogFile); + log.addOutput(new ConsoleOutput(LogLevel.debug)); } private onActivate = () => { @@ -340,6 +351,9 @@ class ApplicationMain { } this.daemonRpc.disconnect(); + + log.dispose(); + this.rendererLog?.dispose(); } private detectLocale(): string { diff --git a/gui/src/main/linux-desktop-entry.ts b/gui/src/main/linux-desktop-entry.ts index 02edd8eb0f..dc1ae0e57b 100644 --- a/gui/src/main/linux-desktop-entry.ts +++ b/gui/src/main/linux-desktop-entry.ts @@ -1,8 +1,8 @@ import child_process from 'child_process'; -import log from 'electron-log'; import fs from 'fs'; import path from 'path'; import { ILinuxApplication } from '../shared/application-types'; +import log from '../shared/logging'; type DirectoryDescription = string | RegExp; diff --git a/gui/src/main/logging.ts b/gui/src/main/logging.ts new file mode 100644 index 0000000000..fb21c54b48 --- /dev/null +++ b/gui/src/main/logging.ts @@ -0,0 +1,103 @@ +import { app } from 'electron'; +import fs from 'fs'; +import path from 'path'; +import { IpcMainEventChannel } from '../shared/ipc-event-channel'; +import { LogLevel, ILogInput, ILogOutput } from '../shared/logging-types'; + +export const OLD_LOG_FILES = ['frontend-renderer.log']; + +export class FileOutput implements ILogOutput { + private fileDescriptor?: number; + + constructor(public level: LogLevel, private filePath: string) { + try { + this.fileDescriptor = fs.openSync(filePath, fs.constants.O_CREAT | fs.constants.O_WRONLY); + } catch (e) { + console.error(`Failed to open ${this.filePath}`); + } + } + + public dispose() { + if (this.fileDescriptor) { + try { + fs.closeSync(this.fileDescriptor); + } catch (e) { + console.error(`Failed to close ${this.filePath}`); + } + } + } + + public write(_level: LogLevel, message: string) { + if (this.fileDescriptor) { + fs.write(this.fileDescriptor, `${message}\n`, (err) => { + if (err) { + console.error(`Failed to log to ${this.filePath}`); + } + }); + } + } +} + +export class IpcInput implements ILogInput { + public on(handler: (level: LogLevel, message: string) => void) { + IpcMainEventChannel.logging.handleLog(({ level, message }) => handler(level, message)); + } +} + +export function getMainLogPath() { + return path.join(getLogDirectoryDir(), 'main.log'); +} + +export function getRendererLogPath() { + return path.join(getLogDirectoryDir(), 'renderer.log'); +} + +export function createLoggingDirectory(): void { + try { + fs.mkdirSync(getLogDirectoryDir(), { recursive: true }); + } catch (e) { + console.error('Failed to create logging directory'); + } +} + +// When cleaning up old log files they are first backed up and the next time removed. +export function cleanUpLogDirectory(fileNames: string[]): void { + fileNames.forEach((fileName) => { + const filePath = path.join(getLogDirectoryDir(), fileName); + rotateOrDeleteFile(filePath); + }); +} + +export function backupLogFile(filePath: string): boolean { + const backupFilePath = getBackupFilePath(filePath); + try { + fs.accessSync(filePath); + fs.renameSync(filePath, backupFilePath); + return true; + } catch (e) { + console.error(`Failed to backup ${filePath}`); + return false; + } +} + +export function rotateOrDeleteFile(filePath: string): void { + if (!backupLogFile(filePath)) { + const backupFilePath = getBackupFilePath(filePath); + try { + fs.accessSync(backupFilePath); + fs.unlinkSync(backupFilePath); + } catch (e) { + console.error(`Failed to delete ${filePath}`); + } + } +} + +function getBackupFilePath(filePath: string): string { + const parsedPath = path.parse(filePath); + parsedPath.base = parsedPath.name + '.old' + parsedPath.ext; + return path.normalize(path.format(parsedPath)); +} + +function getLogDirectoryDir() { + return app.getPath('logs'); +} diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 95c1216b72..17343837d4 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -1,8 +1,8 @@ import { app, nativeImage, NativeImage, Notification } from 'electron'; -import log from 'electron-log'; import os from 'os'; import path from 'path'; import { TunnelState } from '../shared/daemon-rpc-types'; +import log from '../shared/logging'; import { ConnectedNotificationProvider, ConnectingNotificationProvider, diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 73019b1e14..f63e03633a 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -1,4 +1,3 @@ -import log from 'electron-log'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; @@ -21,7 +20,7 @@ import { loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel'; import { ILinuxSplitTunnelingApplication } from '../shared/application-types'; -import { getRendererLogFile, setupLogging } from '../shared/logging'; +import log, { ConsoleOutput } from '../shared/logging'; import consumePromise from '../shared/promise'; import History from './lib/history'; @@ -43,6 +42,8 @@ import { TunnelState, VoucherResponse, } from '../shared/daemon-rpc-types'; +import { LogLevel } from '../shared/logging-types'; +import IpcOutput from './lib/logging'; interface IPreferredLocaleDescriptor { name: string; @@ -94,7 +95,8 @@ export default class AppRenderer { private loginTimer?: NodeJS.Timeout; constructor() { - setupLogging(getRendererLogFile()); + log.addOutput(new ConsoleOutput(LogLevel.debug)); + log.addOutput(new IpcOutput(LogLevel.debug)); IpcRendererEventChannel.locale.listen((locale) => { // load translations for the new locale diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx index 54a3039078..352d70d9e9 100644 --- a/gui/src/renderer/components/AppButton.tsx +++ b/gui/src/renderer/components/AppButton.tsx @@ -1,7 +1,7 @@ -import log from 'electron-log'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../config.json'; +import log from '../../shared/logging'; import { useMounted } from '../lib/utilityHooks'; import { StyledButtonContent, StyledLabel, StyledLabelContainer } from './AppButtonStyles'; import ImageView from './ImageView'; diff --git a/gui/src/renderer/components/ClipboardLabel.tsx b/gui/src/renderer/components/ClipboardLabel.tsx index 8b96b32dd3..2131e884df 100644 --- a/gui/src/renderer/components/ClipboardLabel.tsx +++ b/gui/src/renderer/components/ClipboardLabel.tsx @@ -1,7 +1,7 @@ -import log from 'electron-log'; import * as React from 'react'; import styled from 'styled-components'; import { messages } from '../../shared/gettext'; +import log from '../../shared/logging'; import { Scheduler } from '../../shared/scheduler'; interface IProps { diff --git a/gui/src/renderer/components/ErrorBoundary.tsx b/gui/src/renderer/components/ErrorBoundary.tsx index c54fe97721..040ff71826 100644 --- a/gui/src/renderer/components/ErrorBoundary.tsx +++ b/gui/src/renderer/components/ErrorBoundary.tsx @@ -1,8 +1,8 @@ -import log from 'electron-log'; import React from 'react'; import styled from 'styled-components'; import { colors, links } from '../../config.json'; import { messages } from '../../shared/gettext'; +import log from '../../shared/logging'; import PlatformWindowContainer from '../containers/PlatformWindowContainer'; import ImageView from './ImageView'; import { Container, Layout } from './Layout'; diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index 35fcffdd6d..c087d9984a 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -1,6 +1,6 @@ -import log from 'electron-log'; import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; +import log from '../../shared/logging'; import { BlockWhenDisconnectedNotificationProvider, CloseToAccountExpiryNotificationProvider, diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx index 9e1df9693c..5bb84c7509 100644 --- a/gui/src/renderer/components/WireguardKeys.tsx +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -1,9 +1,9 @@ -import log from 'electron-log'; import moment from 'moment'; import * as React from 'react'; import { sprintf } from 'sprintf-js'; import { TunnelState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import log from '../../shared/logging'; import { IWgKey, WgKeyState } from '../redux/settings/reducers'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index 68d6899acb..0aaa4ee3f4 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,4 +1,3 @@ -import log from 'electron-log'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; import { @@ -7,6 +6,7 @@ import { RelayProtocol, TunnelProtocol, } from '../../shared/daemon-rpc-types'; +import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import AdvancedSettings from '../components/AdvancedSettings'; diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx index f56bc6e160..99882bf8b1 100644 --- a/gui/src/renderer/containers/ConnectPage.tsx +++ b/gui/src/renderer/containers/ConnectPage.tsx @@ -1,8 +1,8 @@ -import log from 'electron-log'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; import { sprintf } from 'sprintf-js'; import { messages } from '../../shared/gettext'; +import log from '../../shared/logging'; import Connect from '../components/Connect'; import withAppContext, { IAppContext } from '../context'; import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers'; diff --git a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx index dcdc862355..8b01b5e996 100644 --- a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx +++ b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx @@ -1,6 +1,6 @@ -import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import log from '../../shared/logging'; import ExpiredAccountErrorView from '../components/ExpiredAccountErrorView'; import accountActions from '../redux/account/actions'; diff --git a/gui/src/renderer/containers/PreferencesPage.tsx b/gui/src/renderer/containers/PreferencesPage.tsx index 35d9c62f5b..571219b528 100644 --- a/gui/src/renderer/containers/PreferencesPage.tsx +++ b/gui/src/renderer/containers/PreferencesPage.tsx @@ -1,6 +1,6 @@ -import log from 'electron-log'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; +import log from '../../shared/logging'; import consumePromise from '../../shared/promise'; import Preferences from '../components/Preferences'; import withAppContext, { IAppContext } from '../context'; diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx index 29bb468304..fbad19285b 100644 --- a/gui/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/src/renderer/containers/SelectLocationPage.tsx @@ -1,9 +1,9 @@ -import log from 'electron-log'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; import { bindActionCreators } from 'redux'; import BridgeSettingsBuilder from '../../shared/bridge-settings-builder'; import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; +import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import SelectLocation from '../components/SelectLocation'; import withAppContext, { IAppContext } from '../context'; diff --git a/gui/src/renderer/lib/logging.ts b/gui/src/renderer/lib/logging.ts new file mode 100644 index 0000000000..433ba20f8f --- /dev/null +++ b/gui/src/renderer/lib/logging.ts @@ -0,0 +1,10 @@ +import { IpcRendererEventChannel } from '../../shared/ipc-event-channel'; +import { ILogOutput, LogLevel } from '../../shared/logging-types'; + +export default class IpcOutput implements ILogOutput { + constructor(public level: LogLevel) {} + + public write(level: LogLevel, message: string) { + IpcRendererEventChannel.logging.log({ level: level, message }); + } +} diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 53d19aba29..59b4c32b46 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -1,4 +1,3 @@ -import log from 'electron-log'; import { BridgeState, KeygenEvent, @@ -9,6 +8,7 @@ import { TunnelProtocol, } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; +import log from '../../../shared/logging'; import { ReduxAction } from '../store'; export type RelaySettingsRedux = diff --git a/gui/src/shared/gettext.ts b/gui/src/shared/gettext.ts index df15e0c2ef..3283269c70 100644 --- a/gui/src/shared/gettext.ts +++ b/gui/src/shared/gettext.ts @@ -1,9 +1,9 @@ -import log from 'electron-log'; import fs from 'fs'; import { po } from 'gettext-parser'; import Gettext from 'node-gettext'; import path from 'path'; import { LocalizationContexts } from './localization-contexts'; +import log from './logging'; const SOURCE_LANGUAGE = 'en'; const LOCALES_DIR = path.resolve(__dirname, '../../locales'); diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 58186a822d..19c11f85ab 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -26,6 +26,12 @@ import { notifyRenderer, send, } from './ipc-helpers'; +import { LogLevel } from './logging-types'; + +interface ILogEntry { + level: LogLevel; + message: string; +} export interface IRelayListPair { relays: IRelayList; @@ -182,6 +188,9 @@ const ipc = { collectLogs: invoke<string[], string>(), sendReport: invoke<{ email: string; message: string; savedReport: string }, void>(), }, + logging: { + log: send<ILogEntry>(), + }, }; export const IpcMainEventChannel = createIpcMain(ipc); diff --git a/gui/src/shared/ipc-helpers.ts b/gui/src/shared/ipc-helpers.ts index 1c1465b09b..d1a72a2df6 100644 --- a/gui/src/shared/ipc-helpers.ts +++ b/gui/src/shared/ipc-helpers.ts @@ -1,6 +1,6 @@ import { ipcMain, ipcRenderer, WebContents } from 'electron'; -import log from 'electron-log'; import { capitalize } from './string-helpers'; +import log from './logging'; type Handler<T, R> = (callback: (arg: T) => R) => void; type Sender<T, R> = (arg: T) => R; diff --git a/gui/src/shared/logging-types.ts b/gui/src/shared/logging-types.ts new file mode 100644 index 0000000000..09bd3f66cf --- /dev/null +++ b/gui/src/shared/logging-types.ts @@ -0,0 +1,17 @@ +export enum LogLevel { + error, + warning, + info, + verbose, + debug, +} + +export interface ILogOutput { + level: LogLevel; + write(level: LogLevel, message: string): void; + dispose?(): void; +} + +export interface ILogInput { + on(handler: (level: LogLevel, message: string) => void): void; +} diff --git a/gui/src/shared/logging.ts b/gui/src/shared/logging.ts index 471fd758c8..48c290c9b5 100644 --- a/gui/src/shared/logging.ts +++ b/gui/src/shared/logging.ts @@ -1,91 +1,63 @@ -import { app, remote } from 'electron'; -import log from 'electron-log'; -import * as fs from 'fs'; -import * as path from 'path'; +import moment from 'moment'; +import { ILogInput, ILogOutput, LogLevel } from './logging-types'; -const LOG_FORMAT = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}][{level}] {text}'; +export class Logger { + private outputs: ILogOutput[] = []; -// Returns platform specific logs folder for application -// See open issue and PR on Github: -// 1. https://github.com/electron/electron/issues/10118 -// 2. https://github.com/electron/electron/pull/10191 -export function getLogsDirectory() { - const theApp = process.type === 'browser' ? app : remote.app; + public addOutput(output: ILogOutput) { + this.outputs.push(output); + } - switch (process.platform) { - case 'darwin': - // macOS: ~/Library/Logs/{appname} - return path.join(theApp.getPath('home'), 'Library/Logs', theApp.name); - default: - // Windows: %LOCALAPPDATA%\{appname}\logs - // Linux: ~/.config/{appname}/logs - return path.join(theApp.getPath('userData'), 'logs'); + public addInput(input: ILogInput) { + input.on((level: LogLevel, message: string) => this.outputMessage(level, message)); } -} -export function getMainLogFile(): string { - return path.join(getLogsDirectory(), 'frontend.log'); -} + public log(level: LogLevel, ...data: unknown[]) { + const time = moment().format('YYYY-MM-DD HH:mm:ss.SSS'); + const message = `[${time}][${LogLevel[level]}] ${data.join(' ')}`; -export function getRendererLogFile(): string { - return path.join(getLogsDirectory(), 'renderer.log'); -} - -export function setupLogging(logFile: string) { - log.transports.console.format = LOG_FORMAT; - log.transports.file.format = LOG_FORMAT; - log.transports.console.level = 'debug'; + this.outputMessage(level, message); + } - if (process.env.NODE_ENV === 'development') { - // Disable log file in development - log.transports.file.level = false; - } else { - // Configure logging to file - log.transports.file.level = 'debug'; - log.transports.file.resolvePath = (_variables) => logFile; + public error = (...data: unknown[]) => this.log(LogLevel.error, ...data); + public warn = (...data: unknown[]) => this.log(LogLevel.warning, ...data); + public info = (...data: unknown[]) => this.log(LogLevel.info, ...data); + public verbose = (...data: unknown[]) => this.log(LogLevel.verbose, ...data); + public debug = (...data: unknown[]) => this.log(LogLevel.debug, ...data); - log.debug(`Logging to ${logFile}`); + public dispose() { + this.outputs.forEach((output) => output.dispose?.()); } -} -export function backupLogFile(filePath: string): boolean { - const exists = fileExists(filePath); - if (exists) { - const backupFilePath = getBackupFilePath(filePath); - fs.renameSync(filePath, backupFilePath); + private outputMessage(level: LogLevel, message: string) { + this.outputs + .filter((output) => level <= output.level) + .forEach((output) => output.write(level, message)); } - - return exists; } -function getBackupFilePath(filePath: string): string { - const parsedPath = path.parse(filePath); - parsedPath.base = parsedPath.name + '.old' + parsedPath.ext; - return path.normalize(path.format(parsedPath)); -} +export class ConsoleOutput implements ILogOutput { + constructor(public level: LogLevel) {} -function fileExists(filePath: string): boolean { - try { - fs.accessSync(filePath); - return true; - } catch { - return false; + public write(level: LogLevel, message: string) { + switch (level) { + case LogLevel.error: + console.error(message); + break; + case LogLevel.warning: + console.warn(message); + break; + case LogLevel.info: + console.info(message); + break; + case LogLevel.verbose: + console.log(message); + break; + case LogLevel.debug: + console.debug(message); + break; + } } } -// When cleaning up old log files they are first backed up and the next time removed. -export function cleanUpLogDirectory(): void { - const oldLogFileNames = ['frontend-renderer.log']; - - oldLogFileNames.forEach((fileName) => { - const filePath = path.join(getLogsDirectory(), fileName); - rotateOrDeleteFile(filePath); - }); -} - -export function rotateOrDeleteFile(filePath: string) { - const backupFilePath = getBackupFilePath(filePath); - if (!backupLogFile(filePath) && fileExists(backupFilePath)) { - fs.unlinkSync(backupFilePath); - } -} +export default new Logger(); diff --git a/gui/test/logging.spec.ts b/gui/test/logging.spec.ts index 9572c6ed24..22c3db02fa 100644 --- a/gui/test/logging.spec.ts +++ b/gui/test/logging.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import fs from 'fs'; import sinon from 'sinon'; import { it, describe, before, beforeEach, after } from 'mocha'; -import * as logging from '../src/shared/logging'; +import { backupLogFile, rotateOrDeleteFile } from '../src/main/logging'; const aPath = 'log-directory/a.log'; const oldAPath = 'log-directory/a.old.log'; @@ -51,7 +51,7 @@ describe('Logging', () => { }); it('should backup log file', () => { - logging.backupLogFile(aPath); + backupLogFile(aPath); const oldA = fs.readFileSync(oldAPath).toString(); expect(fs.accessSync.bind(null, aPath)).to.throw(); @@ -59,7 +59,7 @@ describe('Logging', () => { }); it('should replace backup file', () => { - logging.backupLogFile(bPath); + backupLogFile(bPath); const oldB = fs.readFileSync(oldBPath).toString(); expect(fs.accessSync.bind(null, bPath)).to.throw(); @@ -67,13 +67,13 @@ describe('Logging', () => { }); it('should clean up old log files', () => { - logging.rotateOrDeleteFile(bPath); + rotateOrDeleteFile(bPath); const oldB = fs.readFileSync(oldBPath).toString(); expect(fs.accessSync.bind(null, bPath)).to.throw(); expect(oldB).to.equal(initialFileState[bPath]); - logging.rotateOrDeleteFile(bPath); + rotateOrDeleteFile(bPath); expect(fs.accessSync.bind(null, bPath)).to.throw(); expect(fs.accessSync.bind(null, oldBPath)).to.throw(); diff --git a/gui/test/setup/main.js b/gui/test/setup/main.js deleted file mode 100644 index dd458a30b6..0000000000 --- a/gui/test/setup/main.js +++ /dev/null @@ -1,4 +0,0 @@ -const log = require('electron-log'); - -log.transports.console.level = false; -log.transports.file.level = false; diff --git a/gui/test/setup/renderer.ts b/gui/test/setup/renderer.ts index b8e1c5e2a3..05c236a2da 100644 --- a/gui/test/setup/renderer.ts +++ b/gui/test/setup/renderer.ts @@ -1,13 +1,9 @@ -import log from 'electron-log'; import Enzyme from 'enzyme'; import ReactSixteenAdapter from 'enzyme-adapter-react-16'; import chai from 'chai'; import spies from 'chai-spies'; import chaiAsPromised from 'chai-as-promised'; -log.transports.console.level = false; -log.transports.file.level = false; - chai.use(spies); chai.use(chaiAsPromised); |
