mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
feat: extension error and console logging
This commit is contained in:
parent
9b6f034803
commit
4a243e60a7
8 changed files with 117 additions and 22 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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>) => {
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue