Skip to content

Commit

Permalink
eval: support user setter
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Aug 27, 2020
1 parent e140158 commit a1e75c0
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 135 deletions.
21 changes: 9 additions & 12 deletions packages/plugin-eval-addons/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,22 @@ export function apply(ctx: Context, config: Config) {
const { commands = [] } = manifest
commands.forEach((config) => {
const { name: rawName, desc, options = [], userFields = [] } = config
const { readable = [] } = Array.isArray(userFields) ? { readable: userFields } : userFields

const [name] = rawName.split(' ', 1)
if (!response.commands.includes(name)) {
return logger.warn('unregistered command manifest: %c', name)
}

const cmd = addon
.subcommand(rawName, desc, config)
.option('debug', '启用调试模式', { type: 'boolean' })
.action(async ({ session, command, options }, ...args) => {
const { $app, $user, $uuid } = session
const { name } = command
const user = UserTrap.get($user, readable)
const result = await $app.evalRemote.callAddon($uuid, user, { name, args, options })
return result
})

UserTrap.prepare(cmd, readable)
.option('debug', '启用调试模式', { type: 'boolean', hidden: true })

UserTrap.attach(cmd, userFields, async ({ session, command, options, user, writable }, ...args) => {
const { $app, $uuid } = session
const { name } = command
const result = await $app.evalRemote.callAddon($uuid, user, writable, { name, args, options })
return result
})

options.forEach((config) => {
const { name, desc } = config
cmd.option(name, desc, config)
Expand Down
9 changes: 6 additions & 3 deletions packages/plugin-eval-addons/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ declare module 'koishi-plugin-eval/dist/worker' {
}

interface WorkerAPI {
callAddon(sid: string, user: Partial<User>, argv: AddonArgv): Promise<string | void>
callAddon(sid: string, user: Partial<User>, writable: User.Field[], argv: AddonArgv): Promise<string | void>
}

interface Response {
Expand All @@ -39,10 +39,13 @@ interface AddonContext extends AddonArgv, Context {}
type AddonAction = (ctx: AddonContext) => string | void | Promise<string | void>
const commandMap: Record<string, AddonAction> = {}

WorkerAPI.prototype.callAddon = async function (sid, user, argv) {
WorkerAPI.prototype.callAddon = async function (sid, user, writable, argv) {
const callback = commandMap[argv.name]
try {
return await callback({ ...argv, ...Context(sid, user) })
const context = { ...argv, ...Context(sid, user, writable) }
const result = await callback(context)
await context.user._update()
return result
} catch (error) {
if (!argv.options.debug) return logger.warn(error)
return formatError(error)
Expand Down
119 changes: 46 additions & 73 deletions packages/plugin-eval/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { App, Context, Session, User } from 'koishi-core'
import { CQCode, Logger, defineProperty, Random, pick } from 'koishi-utils'
import { Context, Session, User } from 'koishi-core'
import { Worker, ResourceLimits } from 'worker_threads'
import { CQCode, Logger, defineProperty, Random, pick } from 'koishi-utils'
import { WorkerAPI, WorkerConfig, WorkerData, Response } from './worker'
import { wrap, expose, Remote } from './transfer'
import { MainAPI, Access, UserTrap } from './main'
import { resolve } from 'path'
import { UserTrap } from './trap'

export * from './trap'
export * from './main'

declare module 'koishi-core/dist/app' {
interface App {
Expand Down Expand Up @@ -35,15 +35,15 @@ declare module 'koishi-core/dist/session' {
interface Session {
$uuid: string
_isEval: boolean
_logCount: number
_sendCount: number
}
}

export interface MainConfig {
prefix?: string
timeout?: number
maxLogs?: number
userFields?: User.Field[]
userFields?: Access<User.Field>
resourceLimits?: ResourceLimits
dataKeys?: (keyof WorkerData)[]
}
Expand All @@ -63,33 +63,6 @@ const defaultConfig: EvalConfig = {

const logger = new Logger('eval')

export class MainAPI {
constructor(public app: App) {}

private getSession(uuid: string) {
const session = this.app._sessions[uuid]
if (!session) throw new Error(`session ${uuid} not found`)
return session
}

async execute(uuid: string, message: string) {
const session = this.getSession(uuid)
const send = session.$send
const sendQueued = session.$sendQueued
await session.$execute(message)
session.$sendQueued = sendQueued
session.$send = send
}

async send(uuid: string, message: string) {
const session = this.getSession(uuid)
if (!session._logCount) session._logCount = 0
if (this.app.evalConfig.maxLogs > session._logCount++) {
return await session.$sendQueued(message)
}
}
}

export const workerScript = `require(${JSON.stringify(resolve(__dirname, 'worker.js'))});`

export function apply(ctx: Context, config: Config = {}) {
Expand Down Expand Up @@ -160,54 +133,54 @@ export function apply(ctx: Context, config: Config = {}) {
.before((session) => {
if (!session['_redirected'] && session.$user?.authority < 2) return '权限不足。'
})
.action(async ({ session, options }, expr) => {
if (options.restart) {
await session.$app.evalWorker.terminate()
return '子线程已重启。'
}

if (!expr) return '请输入要执行的脚本。'
expr = CQCode.unescape(expr)
UserTrap.attach(cmd, config.userFields, async ({ session, options, user, writable }, expr) => {
if (options.restart) {
await session.$app.evalWorker.terminate()
return '子线程已重启。'
}

return await new Promise((resolve) => {
logger.debug(expr)
defineProperty(session, '_isEval', true)
if (!expr) return '请输入要执行的脚本。'
expr = CQCode.unescape(expr)

const _resolve = (result?: string) => {
clearTimeout(timer)
app.evalWorker.off('error', listener)
session._isEval = false
resolve(result)
}
return await new Promise((resolve) => {
logger.debug(expr)
defineProperty(session, '_isEval', true)

const timer = setTimeout(async () => {
await app.evalWorker.terminate()
_resolve(!session._logCount && '执行超时。')
}, config.timeout)

const listener = (error: Error) => {
let message = ERROR_CODES[error['code']]
if (!message) {
logger.warn(error)
message = '执行过程中遇到错误。'
}
_resolve(message)
}
const _resolve = (result?: string) => {
clearTimeout(timer)
app.evalWorker.off('error', listener)
session._isEval = false
resolve(result)
}

const timer = setTimeout(async () => {
await app.evalWorker.terminate()
_resolve(!session._sendCount && '执行超时。')
}, config.timeout)

app.evalWorker.on('error', listener)
app.evalRemote.eval({
sid: session.$uuid,
user: UserTrap.get(session.$user, config.userFields),
silent: options.slient,
source: expr,
}).then(_resolve, (error) => {
const listener = (error: Error) => {
let message = ERROR_CODES[error['code']]
if (!message) {
logger.warn(error)
_resolve()
})
message = '执行过程中遇到错误。'
}
_resolve(message)
}

app.evalWorker.on('error', listener)
app.evalRemote.eval({
user,
writable,
sid: session.$uuid,
silent: options.slient,
source: expr,
}).then(_resolve, (error) => {
logger.warn(error)
_resolve()
})
})

UserTrap.prepare(cmd, config.userFields)
})

if (prefix) {
cmd.shortcut(prefix, { oneArg: true, fuzzy: true })
Expand Down
12 changes: 3 additions & 9 deletions packages/plugin-eval/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Helper.function = function (this: Helper, fnc, traps, deepTraps, mock) {
if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key]
if (key === 'constructor') return this.local.Function
if (key === '__proto__') return this.local.Function.prototype
if (key === 'toString' && deepTraps === frozenTraps) return () => `function ${fnc.name}() { [native code] }`
if (key === 'toString' && this === Contextify) return () => `function ${fnc.name}() { [native code] }`
} catch (e) {
// Never pass the handled expcetion through! This block can't throw an exception under normal conditions.
return null
Expand Down Expand Up @@ -622,23 +622,17 @@ const frozenTraps: Trap = createObject({
})

function readonly(value: any, mock: any = {}) {
for (const key in mock) {
const value = mock[key]
if (typeof value === 'function') {
value.toString = () => `function ${value.name}() { [native code] }`
}
}
return Contextify.value(value, null, frozenTraps, mock)
}

export function setGlobal(name: keyof any, value: any, writable = false, configurable = false) {
export function setGlobal(name: keyof any, value: any, writable = false) {
const prop = Contextify.value(name)
try {
Object.defineProperty(GLOBAL, prop, {
value: writable ? Contextify.value(value) : readonly(value),
enumerable: true,
configurable: writable,
writable,
configurable,
})
} catch (e) {
throw Decontextify.value(e)
Expand Down
89 changes: 89 additions & 0 deletions packages/plugin-eval/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { App, Command, CommandAction, ParsedArgv, User } from 'koishi-core'

interface TrappedArgv<O> extends ParsedArgv<never, never, O> {
user: Partial<User>
writable: User.Field[]
}

type TrappedAction<O> = (argv: TrappedArgv<O>, ...args: string[]) => ReturnType<CommandAction>

export interface UserTrap<T = any, K extends User.Field = never> {
fields: Iterable<K>
get(data: Pick<User, K>): T
set(data: Pick<User, K>, value: T): void
}

export namespace UserTrap {
const traps: Record<string, UserTrap<any, any>> = {}

export function define<T, K extends User.Field = never>(key: string, trap: UserTrap<T, K>) {
traps[key] = trap
}

export function attach<O>(command: Command<never, never, O>, fields: Access<User.Field>, action: TrappedAction<O>) {
const { readable = [], writable = [] } = Array.isArray(fields) ? { readable: fields } : fields
for (const field of readable) {
const trap = traps[field]
command.userFields(trap ? trap.fields : [field])
command.action((argv, ...args) => {
const user = get(argv.session.$user, readable)
return action({ ...argv, user, writable }, ...args)
})
}
}

export function get($user: User.Observed<never>, fields: string[]) {
if (!$user) return {}
const result: Partial<User> = {}
for (const field of fields) {
const trap = traps[field]
Reflect.set(result, field, trap ? trap.get($user) : $user[field])
}
return result
}

export function set($user: User.Observed<never>, data: Partial<User>) {
for (const field in data) {
const trap = traps[field]
trap ? trap.set($user, data[field]) : $user[field] = data[field]
}
return $user._update()
}
}

export type Access<T> = T[] | {
readable?: T[]
writable?: T[]
}

export class MainAPI {
constructor(public app: App) {}

private getSession(uuid: string) {
const session = this.app._sessions[uuid]
if (!session) throw new Error(`session ${uuid} not found`)
return session
}

async execute(uuid: string, message: string) {
const session = this.getSession(uuid)
const send = session.$send
const sendQueued = session.$sendQueued
await session.$execute(message)
session.$sendQueued = sendQueued
session.$send = send
}

async send(uuid: string, message: string) {
const session = this.getSession(uuid)
if (!session._sendCount) session._sendCount = 0
if (this.app.evalConfig.maxLogs > session._sendCount++) {
return await session.$sendQueued(message)
}
}

async updateUser(uuid: string, data: Partial<User>) {
const session = this.getSession(uuid)
return UserTrap.set(session.$user, data)
}
}
31 changes: 0 additions & 31 deletions packages/plugin-eval/src/trap.ts

This file was deleted.

Loading

0 comments on commit a1e75c0

Please sign in to comment.