1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

feat: extension error and console logging

This commit is contained in:
Ash Keel 2023-02-02 16:33:57 +01:00
parent 9b6f034803
commit 4a243e60a7
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
8 changed files with 117 additions and 22 deletions

View file

@ -20,7 +20,7 @@ export class Extension extends EventTarget {
private workerStatus = ExtensionStatus.GettingReady; private workerStatus = ExtensionStatus.GettingReady;
private workerError?: ErrorEvent; private workerError?: ErrorEvent | Error;
constructor( constructor(
public readonly info: ExtensionEntry, public readonly info: ExtensionEntry,
@ -34,7 +34,7 @@ export class Extension extends EventTarget {
{ type: 'module' }, { type: 'module' },
); );
this.worker.onerror = (ev) => { this.worker.onerror = (ev) => {
this.workerError = ev; this.status = ExtensionStatus.Error;
this.dispatchEvent(new CustomEvent('error', { detail: ev })); this.dispatchEvent(new CustomEvent('error', { detail: ev }));
}; };
this.worker.onmessage = (ev: MessageEvent<ExtensionHostMessage>) => this.worker.onmessage = (ev: MessageEvent<ExtensionHostMessage>) =>
@ -57,14 +57,27 @@ export class Extension extends EventTarget {
const msg = ev.data; const msg = ev.data;
switch (msg.kind) { switch (msg.kind) {
case 'status-change': case 'status-change':
this.workerStatus = msg.status; this.status = msg.status;
this.dispatchEvent( break;
new CustomEvent('statusChanged', { detail: msg.status }), 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; break;
} }
} }
private set status(newValue: ExtensionStatus) {
this.workerStatus = newValue;
this.dispatchEvent(new CustomEvent('statusChanged', { detail: newValue }));
}
public get status() { public get status() {
return this.workerStatus; return this.workerStatus;
} }
@ -100,14 +113,11 @@ export class Extension extends EventTarget {
} }
stop() { stop() {
if (this.workerStatus === ExtensionStatus.Terminated) { if (this.status === ExtensionStatus.Terminated) {
return; return;
} }
this.worker.terminate(); this.worker.terminate();
this.workerStatus = ExtensionStatus.Terminated; this.status = ExtensionStatus.Terminated;
this.dispatchEvent(
new CustomEvent('statusChanged', { detail: this.workerStatus }),
);
} }
dispose() { dispose() {

View file

@ -22,7 +22,10 @@ export enum ExtensionStatus {
} }
export type ExtensionHostCommand = EHParamMessage | EHStartMessage; export type ExtensionHostCommand = EHParamMessage | EHStartMessage;
export type ExtensionHostMessage = EHStatusChangeMessage; export type ExtensionHostMessage =
| EHStatusChangeMessage
| EHErrorMessage
| EHLogMessage;
interface EHParamMessage { interface EHParamMessage {
kind: 'arguments'; kind: 'arguments';
options: ExtensionRunOptions; options: ExtensionRunOptions;
@ -36,3 +39,17 @@ interface EHStatusChangeMessage {
kind: 'status-change'; kind: 'status-change';
status: ExtensionStatus; status: ExtensionStatus;
} }
interface EHErrorMessage {
kind: 'error';
error: unknown;
}
interface EHLogMessage {
kind: 'log';
level: string;
message: string;
}
export interface LogMessage {
level: string;
message: string;
}

View file

@ -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() { function start() {
if (!extFn || !kv || extensionStatus !== ExtensionStatus.Ready) if (!extFn || !kv || extensionStatus !== ExtensionStatus.Ready)
throw new Error('extension not ready'); throw new Error('extension not ready');
void extFn(kv).then(() => { void extFn(kv)
setStatus(ExtensionStatus.Finished); .then(() => {
}); setStatus(ExtensionStatus.Finished);
})
.catch((error: Error) => {
sendMessage({
kind: 'error',
error,
});
});
setStatus(ExtensionStatus.Running); setStatus(ExtensionStatus.Running);
} }
@ -51,6 +70,12 @@ onmessage = async (ev: MessageEvent<ExtensionHostCommand>) => {
compilerOptions: { module: ts.ModuleKind.CommonJS }, 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 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
extFn = ExtensionFunction.constructor('kv', out.outputText); extFn = ExtensionFunction.constructor('kv', out.outputText);
setStatus(ExtensionStatus.Ready); setStatus(ExtensionStatus.Ready);

View file

@ -331,7 +331,8 @@
"main-loop-finished": "Active", "main-loop-finished": "Active",
"error": "Error encountered", "error": "Error encountered",
"terminated": "Stopped" "terminated": "Stopped"
} },
"error-alert": "Error details for {{name}}"
} }
}, },
"form-actions": { "form-actions": {

View file

@ -6,9 +6,11 @@ import {
ExtensionOptions, ExtensionOptions,
ExtensionRunOptions, ExtensionRunOptions,
ExtensionStatus, ExtensionStatus,
LogMessage,
} from '~/lib/extensions/types'; } from '~/lib/extensions/types';
import { RootState } from '..'; import { RootState } from '..';
import { HTTPConfig } from '../api/types'; import { HTTPConfig } from '../api/types';
import loggingReducer from '../logging/reducer';
interface ExtensionsState { interface ExtensionsState {
ready: boolean; ready: boolean;
@ -127,6 +129,17 @@ export const createExtensionInstance = createAsyncThunk(
payload.dependencies, payload.dependencies,
payload.runOptions, payload.runOptions,
); );
ext.addEventListener('log', (ev: CustomEvent<LogMessage>) => {
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( ext.addEventListener(
'statusChanged', 'statusChanged',
(ev: CustomEvent<ExtensionStatus>) => { (ev: CustomEvent<ExtensionStatus>) => {

View file

@ -41,10 +41,13 @@ const loggingReducer = createSlice({
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) { loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
state.messages = payload state.messages = payload
.map(processEntry) .map(processEntry)
.sort((a, b) => b.time.getTime() - a.time.getTime()); .sort((a, b) => a.time.getTime() - b.time.getTime());
}, },
receivedEvent(state, { payload }: PayloadAction<main.LogEntry>) { receivedEvent(state, { payload }: PayloadAction<main.LogEntry>) {
state.messages = [processEntry(payload), ...state.messages]; state.messages.push(processEntry(payload));
},
uiLogEvent(state, { payload }: PayloadAction<ProcessedLogEntry>) {
state.messages.push(payload);
}, },
clearedEvents(state) { clearedEvents(state) {
state.messages = []; state.messages = [];

View file

@ -366,7 +366,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
viewport={{ maxHeight: 'calc(80vh - 100px)' }} viewport={{ maxHeight: 'calc(80vh - 100px)' }}
> >
<LogEntriesContainer> <LogEntriesContainer>
{filtered.map((entry) => ( {filtered.reverse().map((entry) => (
<LogItem <LogItem
key={entry.caller + entry.time.getTime().toString()} key={entry.caller + entry.time.getTime().toString()}
data={entry} data={entry}

View file

@ -1,5 +1,6 @@
import Editor, { Monaco, useMonaco } from '@monaco-editor/react'; import Editor, { Monaco, useMonaco } from '@monaco-editor/react';
import { import {
ExclamationTriangleIcon,
InfoCircledIcon, InfoCircledIcon,
InputIcon, InputIcon,
PilcrowIcon, PilcrowIcon,
@ -134,6 +135,7 @@ type ExtensionListItemProps = {
enabled: boolean; enabled: boolean;
entry: ExtensionEntry; entry: ExtensionEntry;
status: ExtensionStatus; status: ExtensionStatus;
error?: Error | ErrorEvent;
onEdit: () => void; onEdit: () => void;
onRemove: () => void; onRemove: () => void;
onToggleEnable: () => void; onToggleEnable: () => void;
@ -174,9 +176,32 @@ function ExtensionListItem(props: ExtensionListItemProps) {
props.entry.name props.entry.name
)} )}
{props.enabled ? ( {props.enabled ? (
<ExtensionStatusNote color={colorByStatus(props.status)}> <>
{t(`pages.extensions.statuses.${props.status}`)} <ExtensionStatusNote color={colorByStatus(props.status)}>
</ExtensionStatusNote> {t(`pages.extensions.statuses.${props.status}`)}
</ExtensionStatusNote>
{props.error ? (
<Alert>
<AlertTrigger asChild>
<Button variation="danger" size="small">
<ExclamationTriangleIcon />
</Button>
</AlertTrigger>
<AlertContent
variation="danger"
title={t('pages.extensions.error-alert', {
name: props.entry.name,
})}
actionButtonProps={{
variation: 'danger',
}}
showCancel={false}
>
<code>{props.error.toString()}</code>
</AlertContent>
</Alert>
) : null}
</>
) : null} ) : null}
</ExtensionName> </ExtensionName>
<ExtensionActions> <ExtensionActions>
@ -274,6 +299,7 @@ function ExtensionList({ onNew, onEdit }: ExtensionListProps) {
entry={e} entry={e}
enabled={e.options.enabled} enabled={e.options.enabled}
status={extensions.status[e.name]} status={extensions.status[e.name]}
error={extensions.running[e.name]?.error}
onEdit={() => onEdit(e.name)} onEdit={() => onEdit(e.name)}
onRemove={() => { onRemove={() => {
// Toggle enabled status // Toggle enabled status