Skip to content

Commit

Permalink
feat(Desktop): Add ability to view session logs in app
Browse files Browse the repository at this point in the history
feat(Desktop): Store logs to disk
  • Loading branch information
alex-ketch committed Aug 3, 2021
1 parent 6eb84b6 commit bea03bb
Show file tree
Hide file tree
Showing 28 changed files with 466 additions and 29 deletions.
11 changes: 11 additions & 0 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@stencila/components": "0.39.0",
"@stencila/schema": "1.10.0",
"debounce": "1.2.1",
"electron-log": "^4.4.1",
"electron-squirrel-startup": "1.0.0",
"fp-ts": "2.11.1",
"i18next": "20.3.5",
Expand Down
8 changes: 0 additions & 8 deletions desktop/src/debug.ts

This file was deleted.

5 changes: 5 additions & 0 deletions desktop/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"next": "Finish setup"
}
},
"logs": {
"title": "Application Logs",
"clear": "Clear logs",
"empty": "Nothing has been logged yet"
},
"settings": {
"title": "Settings",
"general": {
Expand Down
3 changes: 0 additions & 3 deletions desktop/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { app, BrowserWindow, protocol } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { debug } from './debug'
import { main, prepare } from './main'
import { requestHandler, scheme } from './main/app-protocol'
import { openLauncherWindow } from './main/launcher/window'
Expand Down Expand Up @@ -52,8 +51,6 @@ protocol.registerSchemesAsPrivileged([
])

if (isDevelopment) {
debug()

app.whenReady().then(() => {
installExtension(REDUX_DEVTOOLS, {
loadExtensionOptions: { allowFileAccess: true },
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/main/global/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export const GLOBAL_CHANNEL = {
OPEN_LINK_IN_DEFAULT_BROWSER: 'OPEN_LINK_IN_DEFAULT_BROWSER',
CAPTURE_ERROR: 'CAPTURE_ERROR',
GET_APP_VERSION: 'GET_APP_VERSION',
UI_READY: 'UI_READY'
UI_READY: 'UI_READY',
} as const
2 changes: 2 additions & 0 deletions desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { enableLogging } from '../preload/logging'
import { enableCrashReports } from '../preload/errors'
import { globalHandlers } from './global'
import { launcherHandlers } from './launcher'
Expand All @@ -12,6 +13,7 @@ import { checkForUpdates } from './utils/update'
* It should configure critical elements which are needed prior to the creation of the main window.
*/
export const prepare = () => {
enableLogging()
enableCrashReports(isReportErrorsEnabled)
setErrorReportingId()
}
Expand Down
5 changes: 5 additions & 0 deletions desktop/src/main/logging/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const LOG_CHANNEL = {
LOGS_WINDOW_OPEN: 'LOGS_WINDOW_OPEN',
LOGS_PRINT: 'LOGS_PRINT',
LOGS_GET: 'LOGS_GET',
} as const
24 changes: 24 additions & 0 deletions desktop/src/main/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ipcMain } from 'electron'
import { CHANNEL } from '../../preload/channels'
import { logStore } from '../../preload/logging'
import { makeHandlers, removeChannelHandlers } from '../utils/handler'
import { valueToSuccessResult } from '../utils/ipc'
import { LOG_CHANNEL } from './channels'
import { showLogs } from './window'

const registerLogHandlers = () => {
ipcMain.handle(CHANNEL.LOGS_WINDOW_OPEN, async () => {
showLogs()
return valueToSuccessResult()
})

ipcMain.handle(CHANNEL.LOGS_GET, async () => {
return valueToSuccessResult(logStore)
})
}

const removeLogHandlers = () => {
removeChannelHandlers(LOG_CHANNEL)
}

export const logHandlers = makeHandlers(registerLogHandlers, removeLogHandlers)
50 changes: 50 additions & 0 deletions desktop/src/main/logging/window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { BrowserWindow } from 'electron'
import { logHandlers } from '.'
import { i18n } from '../../i18n'
import { streamLogsToWindow } from '../../preload/logging'
import { registerBaseMenu } from '../menu'
import { createWindow } from '../window'
import { onUiLoaded } from '../window/windowUtils'

let logsWindow: BrowserWindow | null

const logsUrl = '/logs'

export const showLogs = () => {
if (logsWindow) {
logsWindow.show()
return logsWindow
}

logsWindow = createWindow(logsUrl, {
width: 600,
height: 800,
minWidth: 200,
minHeight: 600,
show: false,
title: i18n.t('logs.title'),
})

// The ID needs to be stored separately from the window object. Otherwise an error
// is thrown because the time remove handlers are called the window object is already destroyed.
const windowId = logsWindow.id

logHandlers.register(windowId)

logsWindow.on('closed', () => {
logHandlers.remove(windowId)
logsWindow = null
})

streamLogsToWindow(logsWindow.webContents)

onUiLoaded(logsWindow.webContents)(() => {
logsWindow?.show()
})

logsWindow.on('focus', () => {
registerBaseMenu()
})

logsWindow?.loadURL(logsUrl)
}
4 changes: 0 additions & 4 deletions desktop/src/main/menu/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { MenuItemConstructorOptions } from 'electron'
export const baseViewMenu: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
Expand Down
17 changes: 17 additions & 0 deletions desktop/src/main/menu/window.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MenuItemConstructorOptions } from 'electron'
import { openLauncherWindow } from '../launcher/window'
import { showLogs } from '../logging/window'
import { isMac } from './utils'

export const baseWindowMenu: MenuItemConstructorOptions = {
Expand All @@ -23,5 +24,21 @@ export const baseWindowMenu: MenuItemConstructorOptions = {
{ role: 'window' as const },
]
: [{ role: 'close' as const }]),
{ type: 'separator' },
{
label: 'Advanced',
submenu: [
{
label: 'Debug Logs',
click: () => {
showLogs()
},
},
{ type: 'separator' },
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
],
},
],
}
6 changes: 2 additions & 4 deletions desktop/src/main/store/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { webContents } from 'electron'
import { CHANNEL } from '../../preload/channels'
import { UnprotectedStoreKeys } from '../../preload/stores'
import { AppConfigStore } from '../../preload/types'
import { sendToAllWindows } from '../utils/ipc'
import { defaultConfigStore, unprotectedStore } from './bootstrap'

export const readAppConfig = () => {
Expand All @@ -22,9 +22,7 @@ export const setAppConfig =
// Inform all open windows of the configuration change.
// Each window should register a listener for the `CHANNEL.CONFIG_APP_SET` and
// react as needed to the changes.
webContents.getAllWebContents().forEach((wc) => {
wc.send(CHANNEL.CONFIG_APP_SET, { key, value })
})
sendToAllWindows(CHANNEL.CONFIG_APP_SET, { key, value })
}

export const updateAppConfig = (newStore: AppConfigStore) => {
Expand Down
12 changes: 11 additions & 1 deletion desktop/src/main/utils/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import { ipcMain, IpcMainInvokeEvent, WebContents, webContents } from 'electron'
import { Result } from 'stencila'
import { InvokeTypes } from '../../preload/types'

Expand All @@ -17,6 +17,16 @@ export function valueToSuccessResult(
}
}

// Send the passed IPC message to all open windows of the configuration change.
// Each window should register a corresponding listener and react as needed to the changes.
export const sendToAllWindows = (
...args: Parameters<WebContents['send']>
): void => {
webContents.getAllWebContents().forEach((wc) => {
wc.send(...args)
})
}

/**
* A wrapper around Electron's `ipcMain.handle` function in order to enable type
* type safe invocation of both the Invoke and Handle aspects.
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/preload/apis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ipcRenderer } from 'electron'
import log from 'electron-log'
import { IpcRendererAPI } from '../preload/types'
import { Channel, Handler, isChannel } from './channels'

Expand All @@ -25,4 +26,5 @@ export const apis: IpcRendererAPI = {
removeAll: (channel: Channel) => {
ipcRenderer.removeAllListeners(channel)
},
log,
}
6 changes: 4 additions & 2 deletions desktop/src/preload/channels.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { UNPROTECTED_STORE_CHANNEL } from '../main/store/channels'
import { CONFIG_CHANNEL } from '../main/config/channels'
import { DOCUMENT_CHANNEL } from '../main/document/channel'
import { GLOBAL_CHANNEL } from '../main/global/channels'
import { LAUNCHER_CHANNEL } from '../main/launcher/channels'
import { LOG_CHANNEL } from '../main/logging/channels'
import { ONBOARDING_CHANNEL } from '../main/onboarding/channels'
import { PROJECT_CHANNEL } from '../main/project/channels'
import { UNPROTECTED_STORE_CHANNEL } from '../main/store/channels'

export const CHANNEL = {
...GLOBAL_CHANNEL,
...UNPROTECTED_STORE_CHANNEL,
...LAUNCHER_CHANNEL,
...CONFIG_CHANNEL,
...LOG_CHANNEL,
...PROJECT_CHANNEL,
...DOCUMENT_CHANNEL,
...ONBOARDING_CHANNEL
...ONBOARDING_CHANNEL,
} as const

export type Channel = keyof typeof CHANNEL
Expand Down
63 changes: 63 additions & 0 deletions desktop/src/preload/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { WebContents } from 'electron'
import log, { LogMessage } from 'electron-log'
import { pubsub } from 'stencila'
import { CHANNEL } from './channels'
import { isDevelopment } from './utils/env'

// In memory store of logs. Reset upon application launch
export const logStore: LogMessage[] = []

// Only add log messages `warn` or higher to log file on disk
log.transports.file.level = 'warn'

// Only log messages `warn` or higher to console/log panel when in production
log.transports.console.level = isDevelopment ? 'silly' : 'warn'

// When a logging a message, store it for future retrieval by the "Application Log" window
log.hooks.push((message: LogMessage, transport): LogMessage => {
if (transport === log.transports.console) {
// Constrain overall length of the array by dropping the oldest log item
if (logStore.length > 200) {
logStore.shift()
}

logStore.push(message)
}
return message
})

/**
* Subscribe to stdout log messages from Stencila CLI client and log to Electron app console.
*/
export const enableLogging = () => {
pubsub.subscribe('logging', (_topic: string, event: any) => {
const {
message,
metadata: { level, target },
} = event

const line = [`%c${target} |%c ${message}`, 'color: blue', 'color: unset']

switch (level) {
case 'TRACE':
return log.verbose(...line)
case 'DEBUG':
return log.debug(...line)
case 'INFO':
return log.info(...line)
case 'WARN':
return log.warn(...line)
case 'ERROR':
return log.error(...line)
}
})
}

export const streamLogsToWindow = (wc: WebContents): void => {
log.hooks.push((message: LogMessage, transport): LogMessage => {
if (transport === log.transports.console) {
wc.send(CHANNEL.LOGS_PRINT, message)
}
return message
})
}

0 comments on commit bea03bb

Please sign in to comment.