导出 VS 工程:实验页工具栏的"导出 EXE"改成下拉按钮"导出 ▾",里面是"导出 EXE"和"导出 VS 工程"。导出的 zip 是教材那样的 VS 2012 窗体应用工程(.sln + .csproj,.NET Framework 4.5),在机房电脑上解压、双击 .sln 用 VS 打开接着改,按 F5 运行。导出前照机房环境编译一遍要放进 zip 的代码:C# 5(VS 2012 的编译器)、真机版设备类库,有错不导出;新版 C# 写法报 CS8026,按功能名给出改法(字符串插值改 string.Format、?. 改先判断 null、=> 改大括号等 12 种)。导出 EXE 仍按最新 C#。平台的 Properties/Resources.Designer.cs 换成 VS 生成的写法和 Resources.resx,图片放 Resources\;代码里按文件名用的素材(ring.mp3)复制到输出目录,运行时在 bin\Debug\Resources\;每个窗体带 .resx(媒体播放器 OcxState 用教材默认状态),SubType、DependentUpon 和 VS 一样;补上 AssemblyInfo、Settings、App.config。教材 dll 全放 lib\,只引用代码直接用到的(和教材工程一样),只带这些编译不过时引用全部;用到书上的 ledHelper.Close() 时带上 LedHelperCompat.cs。文本文件用带 BOM 的 UTF-8 和 CRLF(VS 2012 打开不带 BOM 的文件会把中文当 GBK);zip 文件名用 GBK,另附 Info-ZIP Unicode 文件名字段(0x7075),中文 Windows 自带解压和 7-Zip 都显示正确(ZipWriter.cs)。23 个示例都能导出,导出的工程在 mono 的 msbuild 里全部编译通过、输出目录和教材 bin\Debug 一样、程序能启动;VS 2012 还要在机房实测。接口 POST /api/export-project,代码有错时 422。export-exe.ts 改名 export.ts(服务器和网页各一个)。使用说明、设计方案、README、部署说明同步
Deploy / deploy (push) Failing after 1m8s
Deploy / deploy (push) Failing after 1m8s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c3ce31fe1
commit
2aa267839f
17 files changed
+1209
-80
No files matched your search
+29
-19
@@ -27,7 +27,7 @@ import { config } from './config'
|
||||
import { deleteUserDatabases, restoreExperimentDatabases, userDatabaseDir } from './databases'
|
||||
import { listSamples, loadChecks, loadSample, loadTask } from './samples'
|
||||
import { SqlError, sqlService } from './sql-service'
|
||||
import { exeName, exportExe } from './export-exe'
|
||||
import { type ExportKind, exportProgram, programName } from './export'
|
||||
import { serveStatic } from './static'
|
||||
import { deleteWork, getWork, saveWork, validFiles, validScreenshots } from './works'
|
||||
|
||||
@@ -130,25 +130,35 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
|
||||
app.get('/api/selfcheck', (c) => c.json(selfCheck.get(c.get('user').id)))
|
||||
|
||||
/** 导出 EXE:成功时返回 exe 文件;代码有错误时 422 和报错(照真机版类库编译,和 VS 里一样) */
|
||||
app.post('/api/export-exe', async (c) => {
|
||||
const { files, name } = await body<{ files: unknown; name: unknown }>(c)
|
||||
if (!validFiles(files)) return c.json({ message: '作品格式不对或太大' }, 400)
|
||||
let result
|
||||
try {
|
||||
result = await exportExe(files, exeName(name))
|
||||
} catch (error) {
|
||||
return c.json({ message: error instanceof Error ? error.message : String(error) }, 503)
|
||||
}
|
||||
if (!result.ok) return c.json({ message: '代码有错误,不能导出', diagnostics: result.diagnostics }, 422)
|
||||
return new Response(result.exe, {
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.microsoft.portable-executable',
|
||||
'Content-Disposition': `attachment; filename="program.exe"; filename*=UTF-8''${encodeURIComponent(`${result.name}.exe`)}`,
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
/**
|
||||
* 导出 EXE、VS 工程:成功时返回 exe 文件或工程的 zip;代码有错误时 422 和报错
|
||||
* (照真机版类库编译,和 VS 里一样;导出工程还照机房的 VS 2012 只能用 C# 5)
|
||||
*/
|
||||
const EXPORTS: Record<ExportKind, { ext: string; type: string }> = {
|
||||
exe: { ext: 'exe', type: 'application/vnd.microsoft.portable-executable' },
|
||||
project: { ext: 'zip', type: 'application/zip' },
|
||||
}
|
||||
for (const kind of ['exe', 'project'] as const) {
|
||||
app.post(`/api/export-${kind}`, async (c) => {
|
||||
const { files, name } = await body<{ files: unknown; name: unknown }>(c)
|
||||
if (!validFiles(files)) return c.json({ message: '作品格式不对或太大' }, 400)
|
||||
let result
|
||||
try {
|
||||
result = await exportProgram(files, programName(name), kind)
|
||||
} catch (error) {
|
||||
return c.json({ message: error instanceof Error ? error.message : String(error) }, 503)
|
||||
}
|
||||
if (!result.ok) return c.json({ message: '代码有错误,不能导出', diagnostics: result.diagnostics }, 422)
|
||||
const { ext, type } = EXPORTS[kind]
|
||||
return new Response(result.file, {
|
||||
headers: {
|
||||
'Content-Type': type,
|
||||
'Content-Disposition': `attachment; filename="program.${ext}"; filename*=UTF-8''${encodeURIComponent(`${result.name}.${ext}`)}`,
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 代码编辑器的语言服务:补全、悬停提示、参数提示、边写边检查
|
||||
const LANGUAGE_REQUESTS = new Set(['complete', 'completionDetail', 'hover', 'signature', 'diagnostics'])
|
||||
|
||||
@@ -9,8 +9,9 @@ export interface CompileResult {
|
||||
ms: number
|
||||
}
|
||||
|
||||
/** 导出的 exe 在工程目录里的文件名(Sim.Runner 的 CompileServer.ExportFile) */
|
||||
/** 导出的 exe、VS 工程(zip)在工程目录里的文件名(Sim.Runner 的 CompileServer.ExportFile、ExportProjectFile) */
|
||||
export const EXPORT_FILE = 'export.exe'
|
||||
export const EXPORT_PROJECT_FILE = 'export.zip'
|
||||
|
||||
type Reply = ({ type: 'compiled' } & CompileResult) | { type: 'compileFailed'; message: string }
|
||||
|
||||
@@ -37,6 +38,14 @@ class CompileService {
|
||||
return this.request({ dir, export: { libs: config.textbookLibsDir, name } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 VS 工程:照机房的 VS 2012(C# 5、.NET Framework 4.5、真机版设备类库)检查,
|
||||
* 打包成 zip 写到工程目录的 EXPORT_PROJECT_FILE。name 是工程名
|
||||
*/
|
||||
exportProject(dir: string, name: string): Promise<CompileResult> {
|
||||
return this.request({ dir, export: { kind: 'project', libs: config.textbookLibsDir, name } })
|
||||
}
|
||||
|
||||
private async request(payload: object): Promise<CompileResult> {
|
||||
await this.start()
|
||||
const id = this.nextId++
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
// 导出 EXE:学生工程照教材工程编译成 Windows 程序(嵌入教材的真机版设备类库),在机房双击就能控制真的实训台。
|
||||
// 导出工程:打包成教材那样的 VS 2012 工程(zip),在机房电脑上用 VS 打开接着改。
|
||||
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import type { Diagnostic, SourceFile } from '@iot/shared'
|
||||
import { compileService, EXPORT_FILE } from './compile-service'
|
||||
import { compileService, EXPORT_FILE, EXPORT_PROJECT_FILE } from './compile-service'
|
||||
import { config } from './config'
|
||||
import { safeJoin } from './run-session'
|
||||
|
||||
export type ExportResult = { ok: true; exe: Uint8Array; name: string } | { ok: false; diagnostics: Diagnostic[] }
|
||||
export type ExportResult = { ok: true; file: Uint8Array; name: string } | { ok: false; diagnostics: Diagnostic[] }
|
||||
|
||||
/** exe 的名字:只留文字、数字、下划线和减号(也是程序集名),空了就叫"上位机程序" */
|
||||
export function exeName(name: unknown) {
|
||||
export type ExportKind = 'exe' | 'project'
|
||||
|
||||
/** exe、工程的名字:只留文字、数字、下划线和减号(也是程序集名),空了就叫"上位机程序" */
|
||||
export function programName(name: unknown) {
|
||||
const cleaned = typeof name === 'string' ? name.replace(/[^\p{L}\p{N}_-]/gu, '').slice(0, 40) : ''
|
||||
return cleaned || '上位机程序'
|
||||
}
|
||||
|
||||
/** 编译失败返回报错;服务器上没有教材 dll 等情况抛出异常(消息给学生看) */
|
||||
export async function exportExe(files: SourceFile[], name: string): Promise<ExportResult> {
|
||||
export async function exportProgram(files: SourceFile[], name: string, kind: ExportKind): Promise<ExportResult> {
|
||||
await mkdir(config.runsDir, { recursive: true })
|
||||
const dir = await mkdtemp(join(config.runsDir, 'export-'))
|
||||
try {
|
||||
@@ -24,9 +27,9 @@ export async function exportExe(files: SourceFile[], name: string): Promise<Expo
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
await Bun.write(target, file.content)
|
||||
}
|
||||
const result = await compileService.exportExe(dir, name)
|
||||
const result = kind === 'project' ? await compileService.exportProject(dir, name) : await compileService.exportExe(dir, name)
|
||||
if (!result.ok) return { ok: false, diagnostics: result.diagnostics }
|
||||
return { ok: true, exe: await Bun.file(join(dir, EXPORT_FILE)).bytes(), name }
|
||||
return { ok: true, file: await Bun.file(join(dir, kind === 'project' ? EXPORT_PROJECT_FILE : EXPORT_FILE)).bytes(), name }
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
}
|
||||
Reference in new issue
Block a user