From 4a243e60a707002693e550291dacc4aeee5dbe79 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Thu, 2 Feb 2023 16:33:57 +0100 Subject: [PATCH] feat: extension error and console logging --- frontend/src/lib/extensions/extension.ts | 32 ++++++++++++------- frontend/src/lib/extensions/types.ts | 19 ++++++++++- .../lib/extensions/workers/extensionHost.ts | 31 ++++++++++++++++-- frontend/src/locale/en/translation.json | 3 +- frontend/src/store/extensions/reducer.ts | 13 ++++++++ frontend/src/store/logging/reducer.ts | 7 ++-- frontend/src/ui/components/LogViewer.tsx | 2 +- frontend/src/ui/pages/Extensions.tsx | 32 +++++++++++++++++-- 8 files changed, 117 insertions(+), 22 deletions(-) diff --git a/frontend/src/lib/extensions/extension.ts b/frontend/src/lib/extensions/extension.ts index 3d4382d..95f36ec 100644 --- a/frontend/src/lib/extensions/extension.ts +++ b/frontend/src/lib/extensions/extension.ts @@ -20,7 +20,7 @@ export class Extension extends EventTarget { private workerStatus = ExtensionStatus.GettingReady; - private workerError?: ErrorEvent; + private workerError?: ErrorEvent | Error; constructor( public readonly info: ExtensionEntry, @@ -34,7 +34,7 @@ export class Extension extends EventTarget { { type: 'module' }, ); this.worker.onerror = (ev) => { - this.workerError = ev; + this.status = ExtensionStatus.Error; this.dispatchEvent(new CustomEvent('error', { detail: ev })); }; this.worker.onmessage = (ev: MessageEvent) => @@ -57,14 +57,27 @@ export class Extension extends EventTarget { const msg = ev.data; switch (msg.kind) { case 'status-change': - this.workerStatus = msg.status; - this.dispatchEvent( - new CustomEvent('statusChanged', { detail: msg.status }), - ); + this.status = msg.status; + break; + case 'error': + if (msg.error instanceof Error) { + this.workerError = msg.error; + } else { + this.workerError = new Error(msg.error.toString()); + } + this.status = ExtensionStatus.Error; + break; + case 'log': + this.dispatchEvent(new CustomEvent('log', { detail: msg })); break; } } + private set status(newValue: ExtensionStatus) { + this.workerStatus = newValue; + this.dispatchEvent(new CustomEvent('statusChanged', { detail: newValue })); + } + public get status() { return this.workerStatus; } @@ -100,14 +113,11 @@ export class Extension extends EventTarget { } stop() { - if (this.workerStatus === ExtensionStatus.Terminated) { + if (this.status === ExtensionStatus.Terminated) { return; } this.worker.terminate(); - this.workerStatus = ExtensionStatus.Terminated; - this.dispatchEvent( - new CustomEvent('statusChanged', { detail: this.workerStatus }), - ); + this.status = ExtensionStatus.Terminated; } dispose() { diff --git a/frontend/src/lib/extensions/types.ts b/frontend/src/lib/extensions/types.ts index f938180..cd6f882 100644 --- a/frontend/src/lib/extensions/types.ts +++ b/frontend/src/lib/extensions/types.ts @@ -22,7 +22,10 @@ export enum ExtensionStatus { } export type ExtensionHostCommand = EHParamMessage | EHStartMessage; -export type ExtensionHostMessage = EHStatusChangeMessage; +export type ExtensionHostMessage = + | EHStatusChangeMessage + | EHErrorMessage + | EHLogMessage; interface EHParamMessage { kind: 'arguments'; options: ExtensionRunOptions; @@ -36,3 +39,17 @@ interface EHStatusChangeMessage { kind: 'status-change'; status: ExtensionStatus; } +interface EHErrorMessage { + kind: 'error'; + error: unknown; +} +interface EHLogMessage { + kind: 'log'; + level: string; + message: string; +} + +export interface LogMessage { + level: string; + message: string; +} diff --git a/frontend/src/lib/extensions/workers/extensionHost.ts b/frontend/src/lib/extensions/workers/extensionHost.ts index 9c06ce0..def1398 100644 --- a/frontend/src/lib/extensions/workers/extensionHost.ts +++ b/frontend/src/lib/extensions/workers/extensionHost.ts @@ -26,12 +26,31 @@ function setStatus(status: ExtensionStatus) { }); } +function log(level: string) { + // eslint-disable-next-line func-names + return function (...args: { toString(): string }[]) { + const message = args.join(' '); + sendMessage({ + kind: 'log', + level, + message, + }); + }; +} + function start() { if (!extFn || !kv || extensionStatus !== ExtensionStatus.Ready) throw new Error('extension not ready'); - void extFn(kv).then(() => { - setStatus(ExtensionStatus.Finished); - }); + void extFn(kv) + .then(() => { + setStatus(ExtensionStatus.Finished); + }) + .catch((error: Error) => { + sendMessage({ + kind: 'error', + error, + }); + }); setStatus(ExtensionStatus.Running); } @@ -51,6 +70,12 @@ onmessage = async (ev: MessageEvent) => { compilerOptions: { module: ts.ModuleKind.CommonJS }, }); + // Replace console.* methods with something that logs to UI + console.log = log('info'); + console.info = log('info'); + console.warn = log('warn'); + console.error = log('error'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment extFn = ExtensionFunction.constructor('kv', out.outputText); setStatus(ExtensionStatus.Ready); diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index 9128ff5..aefdb68 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -331,7 +331,8 @@ "main-loop-finished": "Active", "error": "Error encountered", "terminated": "Stopped" - } + }, + "error-alert": "Error details for {{name}}" } }, "form-actions": { diff --git a/frontend/src/store/extensions/reducer.ts b/frontend/src/store/extensions/reducer.ts index 2057d86..0814947 100644 --- a/frontend/src/store/extensions/reducer.ts +++ b/frontend/src/store/extensions/reducer.ts @@ -6,9 +6,11 @@ import { ExtensionOptions, ExtensionRunOptions, ExtensionStatus, + LogMessage, } from '~/lib/extensions/types'; import { RootState } from '..'; import { HTTPConfig } from '../api/types'; +import loggingReducer from '../logging/reducer'; interface ExtensionsState { ready: boolean; @@ -127,6 +129,17 @@ export const createExtensionInstance = createAsyncThunk( payload.dependencies, payload.runOptions, ); + ext.addEventListener('log', (ev: CustomEvent) => { + dispatch( + loggingReducer.actions.uiLogEvent({ + time: new Date(), + caller: `extensionHost/${payload.entry.name}`, + level: ev.detail.level, + message: ev.detail.message, + data: { extension: payload.entry.name }, + }), + ); + }); ext.addEventListener( 'statusChanged', (ev: CustomEvent) => { diff --git a/frontend/src/store/logging/reducer.ts b/frontend/src/store/logging/reducer.ts index 6a01261..687d3f4 100644 --- a/frontend/src/store/logging/reducer.ts +++ b/frontend/src/store/logging/reducer.ts @@ -41,10 +41,13 @@ const loggingReducer = createSlice({ loadedLogData(state, { payload }: PayloadAction) { state.messages = payload .map(processEntry) - .sort((a, b) => b.time.getTime() - a.time.getTime()); + .sort((a, b) => a.time.getTime() - b.time.getTime()); }, receivedEvent(state, { payload }: PayloadAction) { - state.messages = [processEntry(payload), ...state.messages]; + state.messages.push(processEntry(payload)); + }, + uiLogEvent(state, { payload }: PayloadAction) { + state.messages.push(payload); }, clearedEvents(state) { state.messages = []; diff --git a/frontend/src/ui/components/LogViewer.tsx b/frontend/src/ui/components/LogViewer.tsx index 22b9220..4789414 100644 --- a/frontend/src/ui/components/LogViewer.tsx +++ b/frontend/src/ui/components/LogViewer.tsx @@ -366,7 +366,7 @@ function LogDialog({ initialFilter }: LogDialogProps) { viewport={{ maxHeight: 'calc(80vh - 100px)' }} > - {filtered.map((entry) => ( + {filtered.reverse().map((entry) => ( void; onRemove: () => void; onToggleEnable: () => void; @@ -174,9 +176,32 @@ function ExtensionListItem(props: ExtensionListItemProps) { props.entry.name )} {props.enabled ? ( - - {t(`pages.extensions.statuses.${props.status}`)} - + <> + + {t(`pages.extensions.statuses.${props.status}`)} + + {props.error ? ( + + + + + + {props.error.toString()} + + + ) : null} + ) : null} @@ -274,6 +299,7 @@ function ExtensionList({ onNew, onEdit }: ExtensionListProps) { entry={e} enabled={e.options.enabled} status={extensions.status[e.name]} + error={extensions.running[e.name]?.error} onEdit={() => onEdit(e.name)} onRemove={() => { // Toggle enabled status