diff --git a/server/src/databases.ts b/server/src/databases.ts index 18c29ff..5cfc657 100644 --- a/server/src/databases.ts +++ b/server/src/databases.ts @@ -27,16 +27,19 @@ export async function runScript(dir: string, script: string) { /** * 把学生某个实验用到的库恢复成实验的初始数据:只替换脚本里建的库,学生的其他库不动。 + * onlyMissing 时只建学生还没有的库(第一次运行实验时自动建库,已有的库保留学生的数据)。 * 返回恢复了哪些库。 */ -export async function restoreExperimentDatabases(userId: number, sampleId: string) { +export async function restoreExperimentDatabases(userId: number, sampleId: string, onlyMissing = false) { const script = await experimentScript(sampleId) if (!script) return [] + const target = await userDatabaseDir(userId) + const existing = new Set(await readdir(target)) + if (onlyMissing && scriptDatabases(script).every((name) => existing.has(`${name}.db`))) return [] const temp = await mkdtemp(join(config.runsDir, 'db-')) try { await runScript(temp, script) - const target = await userDatabaseDir(userId) - const files = (await readdir(temp)).filter((f) => f.endsWith('.db') && f !== 'master.db') + const files = (await readdir(temp)).filter((f) => f.endsWith('.db') && f !== 'master.db' && !(onlyMissing && existing.has(f))) for (const file of files) { for (const suffix of ['', '-wal', '-shm']) await rm(join(target, file + suffix), { force: true }) await cp(join(temp, file), join(target, file)) @@ -47,6 +50,11 @@ export async function restoreExperimentDatabases(userId: number, sampleId: strin } } +/** 脚本里 CREATE DATABASE 建的库名 */ +function scriptDatabases(script: string) { + return [...script.matchAll(/^\s*CREATE\s+DATABASE\s+\[?([^\s\]]+)\]?/gim)].map((m) => m[1]) +} + /** 评分用:实验数据库的模板按实验缓存,每个检查项复制一份(互不影响) */ const templates = new Map>() diff --git a/server/src/index.ts b/server/src/index.ts index caefc13..720eff8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -7,7 +7,8 @@ import { languageService } from './language-service' import { config } from './config' import { openDatabase } from './db' import { createGradingQueue } from './grading/queue' -import { userDatabaseDir } from './databases' +import { restoreExperimentDatabases, userDatabaseDir } from './databases' +import { listSamples } from './samples' import { cleanupStaleRuns, RunSession } from './run-session' import { sqlService } from './sql-service' import { checkSandbox, sandboxError } from './sandbox' @@ -56,6 +57,11 @@ const server = Bun.serve({ if (!session) return if (message.type === 'run') { try { + const { sampleId } = message + if (typeof sampleId === 'string' && (await listSamples()).some((s) => s.id === sampleId)) { + // 建库失败不影响运行(程序连不上库时会报出来) + await restoreExperimentDatabases(ws.data.userId!, sampleId, true).catch((error) => console.error('自动建实验数据库失败', error)) + } await session.start(message.files) } catch (error) { ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage)) diff --git a/shared/protocol.ts b/shared/protocol.ts index eb6c530..37d711c 100644 --- a/shared/protocol.ts +++ b/shared/protocol.ts @@ -155,7 +155,8 @@ export type RunnerInput = /** 浏览器发给服务器的消息 */ export type ClientMessage = - | { type: 'run'; files: SourceFile[] } + /** sampleId:当前实验,第一次运行时自动建好实验预置的数据库(学生还没有这些库时) */ + | { type: 'run'; files: SourceFile[]; sampleId?: string } /** 调节实训台环境:拖动滑块、触发传感器 */ /** clock:拨到某个时刻(time:yyyy-MM-ddTHH:mm:ss)、改倍速,或 null 恢复真实时间 */ | { type: 'boardInput'; analog?: Partial; digital?: Partial; clock?: { time?: string; speed?: number } | null } diff --git a/web/src/pages/WorkbenchPage.vue b/web/src/pages/WorkbenchPage.vue index de16083..66413cd 100644 --- a/web/src/pages/WorkbenchPage.vue +++ b/web/src/pages/WorkbenchPage.vue @@ -409,7 +409,10 @@ function restoreOriginal() { function run() { if (running.value) return - session.run(files.value.map((f) => ({ ...f }))) + session.run( + files.value.map((f) => ({ ...f })), + props.assignmentId ? assignment.value?.sampleId : sampleId.value, + ) } diff --git a/web/src/run/session.ts b/web/src/run/session.ts index a6cc956..479fb00 100644 --- a/web/src/run/session.ts +++ b/web/src/run/session.ts @@ -109,7 +109,8 @@ export function createRunSession() { socket?.send(JSON.stringify(message)) } - async function run(files: SourceFile[]) { + /** sampleId:当前实验(服务器据此在第一次运行时建好实验预置的数据库) */ + async function run(files: SourceFile[], sampleId?: string) { await connect() state.diagnostics = [] state.console = '' @@ -121,7 +122,7 @@ export function createRunSession() { nodes.clear() for (const url of images.values()) URL.revokeObjectURL(url) images.clear() - send({ type: 'run', files }) + send({ type: 'run', files, sampleId }) } /** 调节实训台环境(滑块、传感器开关) */