Skip to content

Commit

Permalink
fix(eval): preserver storage backup, fix #225
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 12, 2021
1 parent 46a1709 commit 2b6cc3b
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 21 deletions.
4 changes: 2 additions & 2 deletions packages/plugin-eval/src/main.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -72,7 +72,7 @@ export namespace Trap {
export type Access<T> = T[] | AccessObject<T>

interface Argv<A extends any[], O> extends IArgv<never, never, A, O> {
scope?: ScopeData
scope?: SessionData
}

type Action<A extends any[], O> = (argv: Argv<A, O>, ...args: A) => ReturnType<Command.Action>
Expand Down
37 changes: 24 additions & 13 deletions packages/plugin-eval/src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -81,7 +81,7 @@ export function formatError(error: Error) {

const main = wrap<MainHandle>(parentPort)

export interface ScopeData {
export interface SessionData {
id: string
user: Partial<User>
channel: Partial<Channel>
Expand All @@ -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<any>
channel: Channel.Observed<any>
Expand All @@ -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) => {
Expand Down Expand Up @@ -148,7 +149,7 @@ interface AddonArgv {
options: Record<string, any>
}

interface AddonScope extends AddonArgv, Scope {}
interface AddonScope extends AddonArgv, Session {}

type AddonAction = (scope: AddonScope) => string | void | Promise<string | void>
const commandMap: Record<string, AddonAction> = {}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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'))
Expand Down
14 changes: 8 additions & 6 deletions packages/plugin-eval/src/worker/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ interface Module {
export const modules: Record<string, Module> = {}
export const synthetics: Record<string, Module> = {}

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]))
}
}, { context, identifier })
modules[identifier] = module
config.addonNames?.unshift(identifier)
if (name) synthetics[name] = module
if (globalName) synthetics[globalName] = module
}

const suffixes = ['', '.ts', '/index.ts']
Expand Down Expand Up @@ -73,11 +73,13 @@ const V8_TAG = cachedDataVersionTag()
const files: Record<string, FileCache> = {}
const cachedFiles: Record<string, FileCache> = {}

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
Expand All @@ -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)
}
Expand Down

0 comments on commit 2b6cc3b

Please sign in to comment.