diff --git a/packages/plugin-eval/src/main.ts b/packages/plugin-eval/src/main.ts index 71aa9dca0d..ab040128b9 100644 --- a/packages/plugin-eval/src/main.ts +++ b/packages/plugin-eval/src/main.ts @@ -1,7 +1,7 @@ import { App, Command, Channel, Argv as IArgv, User, Context } from 'koishi-core' import { Logger, Observed, pick, union } from 'koishi-utils' import { Worker, ResourceLimits } from 'worker_threads' -import { WorkerHandle, WorkerConfig, WorkerData, ScopeData } from './worker' +import { WorkerHandle, WorkerConfig, WorkerData, SessionData } from './worker' import { expose, Remote, wrap } from './transfer' import { resolve } from 'path' @@ -72,7 +72,7 @@ export namespace Trap { export type Access = T[] | AccessObject interface Argv extends IArgv { - scope?: ScopeData + scope?: SessionData } type Action = (argv: Argv, ...args: A) => ReturnType diff --git a/packages/plugin-eval/src/worker/index.ts b/packages/plugin-eval/src/worker/index.ts index 54d9c3c1cf..6a6f694412 100644 --- a/packages/plugin-eval/src/worker/index.ts +++ b/packages/plugin-eval/src/worker/index.ts @@ -3,7 +3,7 @@ import { parentPort, workerData } from 'worker_threads' import { InspectOptions, formatWithOptions } from 'util' import { findSourceMap } from 'module' import { resolve, dirname, sep } from 'path' -import { serialize } from 'v8' +import { deserialize, serialize } from 'v8' /* eslint-disable import/first */ @@ -81,7 +81,7 @@ export function formatError(error: Error) { const main = wrap(parentPort) -export interface ScopeData { +export interface SessionData { id: string user: Partial channel: Partial @@ -92,7 +92,7 @@ export interface ScopeData { type Serializable = string | number | boolean | Serializable[] | SerializableObject type SerializableObject = { [K in string]: Serializable } -export interface Scope { +export interface Session { storage: SerializableObject user: User.Observed channel: Channel.Observed @@ -101,9 +101,10 @@ export interface Scope { } let storage: SerializableObject +let backupStorage: Buffer const storagePath = resolve(config.root || process.cwd(), config.storageFile || '.koishi/storage') -export const Scope = ({ id, user, userWritable, channel, channelWritable }: ScopeData): Scope => ({ +export const createSession = ({ id, user, userWritable, channel, channelWritable }: SessionData): Session => ({ storage, user: user && observe(user, async (diff) => { @@ -148,7 +149,7 @@ interface AddonArgv { options: Record } -interface AddonScope extends AddonArgv, Scope {} +interface AddonScope extends AddonArgv, Session {} type AddonAction = (scope: AddonScope) => string | void | Promise const commandMap: Record = {} @@ -158,18 +159,23 @@ export class WorkerHandle { return response } - async sync(scope: Scope) { + async sync(scope: Session) { await scope.user?._update() await scope.channel?._update() - const buffer = serialize(scope.storage) - await safeWriteFile(storagePath, buffer) + try { + const buffer = serialize(scope.storage) + await safeWriteFile(storagePath, backupStorage = buffer) + } catch (error) { + storage = deserialize(backupStorage) + throw error + } } - async eval(data: ScopeData, options: EvalOptions) { + async eval(data: SessionData, options: EvalOptions) { const { source, silent } = options const key = 'koishi-eval-context:' + data.id - const scope = Scope(data) + const scope = createSession(data) internal.setGlobal(Symbol.for(key), scope, true) let result: any @@ -189,10 +195,10 @@ export class WorkerHandle { return formatResult(result) } - async callAddon(options: ScopeData, argv: AddonArgv) { + async callAddon(options: SessionData, argv: AddonArgv) { const callback = commandMap[argv.name] try { - const ctx = { ...argv, ...Scope(options) } + const ctx = { ...argv, ...createSession(options) } const result = await callback(ctx) await this.sync(ctx) return result @@ -224,7 +230,12 @@ Object.values(config.setupFiles).map(require) async function start() { const data = await Promise.all([readSerialized(storagePath), prepare()]) - storage = data[0] || {} + storage = data[0][0] + backupStorage = data[0][1] + if (!storage) { + storage = {} + backupStorage = serialize({}) + } response.commands = Object.keys(commandMap) mapDirectory('koishi/utils/', require.resolve('koishi-utils')) diff --git a/packages/plugin-eval/src/worker/loader.ts b/packages/plugin-eval/src/worker/loader.ts index 5ada828d80..92625b31f1 100644 --- a/packages/plugin-eval/src/worker/loader.ts +++ b/packages/plugin-eval/src/worker/loader.ts @@ -22,7 +22,7 @@ interface Module { export const modules: Record = {} export const synthetics: Record = {} -export function synthetize(identifier: string, namespace: {}, name?: string) { +export function synthetize(identifier: string, namespace: {}, globalName?: string) { const module = new SyntheticModule(Object.keys(namespace), function () { for (const key in namespace) { this.setExport(key, internal.contextify(namespace[key])) @@ -30,7 +30,7 @@ export function synthetize(identifier: string, namespace: {}, name?: string) { }, { context, identifier }) modules[identifier] = module config.addonNames?.unshift(identifier) - if (name) synthetics[name] = module + if (globalName) synthetics[globalName] = module } const suffixes = ['', '.ts', '/index.ts'] @@ -73,11 +73,13 @@ const V8_TAG = cachedDataVersionTag() const files: Record = {} const cachedFiles: Record = {} -export async function readSerialized(filename: string) { +export async function readSerialized(filename: string): Promise<[any?, Buffer?]> { try { const buffer = await fs.readFile(filename) - return deserialize(buffer) - } catch {} + return [deserialize(buffer), buffer] + } catch { + return [] + } } // errors should be catched because we should not expose file paths to users @@ -97,7 +99,7 @@ export default async function prepare() { const cachePath = resolve(config.root, config.cacheFile || '.koishi/cache') await Promise.all([ loader.prepare?.(config), - readSerialized(cachePath).then((data) => { + readSerialized(cachePath).then(([data]) => { if (data && data.tag === CACHE_TAG && data.v8tag === V8_TAG) { Object.assign(cachedFiles, data.files) }