@exercode/problem-utils 4.3.0 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/checkAllProblems.cjs.map +1 -1
- package/dist/cli/checkAllProblems.js.map +1 -1
- package/dist/cli/exercodeProblem.cjs.map +1 -1
- package/dist/cli/exercodeProblem.js.map +1 -1
- package/dist/cli/runSingleHarness.cjs.map +1 -1
- package/dist/cli/runSingleHarness.js.map +1 -1
- package/dist/cli/validateLearningMaterial.cjs.map +1 -1
- package/dist/cli/validateLearningMaterial.js.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
- package/dist/helpers/defaultStdioHarness.cjs.map +1 -1
- package/dist/helpers/defaultStdioHarness.js.map +1 -1
- package/dist/helpers/findEntryPointFile.cjs.map +1 -1
- package/dist/helpers/findEntryPointFile.js.map +1 -1
- package/dist/helpers/findLanguageDefinitionByPath.cjs.map +1 -1
- package/dist/helpers/findLanguageDefinitionByPath.js.map +1 -1
- package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
- package/dist/helpers/findModelAnswerDirs.js.map +1 -1
- package/dist/helpers/guiRecording.cjs.map +1 -1
- package/dist/helpers/guiRecording.js.map +1 -1
- package/dist/helpers/javaJudge.cjs.map +1 -1
- package/dist/helpers/javaJudge.js.map +1 -1
- package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
- package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
- package/dist/helpers/parseArgs.cjs.map +1 -1
- package/dist/helpers/parseArgs.js.map +1 -1
- package/dist/helpers/parseCsv.cjs.map +1 -1
- package/dist/helpers/parseCsv.js.map +1 -1
- package/dist/helpers/printDebugBanner.cjs.map +1 -1
- package/dist/helpers/printDebugBanner.js.map +1 -1
- package/dist/helpers/printTestCaseResult.cjs.map +1 -1
- package/dist/helpers/printTestCaseResult.js.map +1 -1
- package/dist/helpers/readOutputFiles.cjs.map +1 -1
- package/dist/helpers/readOutputFiles.js.map +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
- package/dist/helpers/readTestCases.cjs.map +1 -1
- package/dist/helpers/readTestCases.js.map +1 -1
- package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
- package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
- package/dist/helpers/resolveCwds.cjs.map +1 -1
- package/dist/helpers/resolveCwds.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/runHarnessProcess.cjs.map +1 -1
- package/dist/helpers/runHarnessProcess.js.map +1 -1
- package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -1
- package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -1
- package/dist/helpers/runPythonInTemporaryUvProject.cjs.map +1 -1
- package/dist/helpers/runPythonInTemporaryUvProject.js.map +1 -1
- package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
- package/dist/helpers/sourceCodeGrammars.js.map +1 -1
- package/dist/helpers/spawnWithLimits.cjs.map +1 -1
- package/dist/helpers/spawnWithLimits.js.map +1 -1
- package/dist/helpers/spawnWithTimeout.cjs.map +1 -1
- package/dist/helpers/spawnWithTimeout.js.map +1 -1
- package/dist/helpers/startHttpServer.cjs.map +1 -1
- package/dist/helpers/startHttpServer.js.map +1 -1
- package/dist/helpers/stdioJudgeRules.cjs.map +1 -1
- package/dist/helpers/stdioJudgeRules.js.map +1 -1
- package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
- package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
- package/dist/learningMaterial/frontmatter.cjs.map +1 -1
- package/dist/learningMaterial/frontmatter.js.map +1 -1
- package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
- package/dist/learningMaterial/fsHelpers.js.map +1 -1
- package/dist/learningMaterial/schemas.cjs.map +1 -1
- package/dist/learningMaterial/schemas.js.map +1 -1
- package/dist/learningMaterial/validateContest.cjs.map +1 -1
- package/dist/learningMaterial/validateContest.js.map +1 -1
- package/dist/learningMaterial/validateCourse.cjs.map +1 -1
- package/dist/learningMaterial/validateCourse.js.map +1 -1
- package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
- package/dist/learningMaterial/validateMaterial.js.map +1 -1
- package/dist/learningMaterial/validateProblem.cjs.map +1 -1
- package/dist/learningMaterial/validateProblem.js.map +1 -1
- package/dist/learningMaterial/validationResult.cjs.map +1 -1
- package/dist/learningMaterial/validationResult.js.map +1 -1
- package/dist/presets/command.cjs.map +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/evaluation.cjs.map +1 -1
- package/dist/presets/evaluation.js.map +1 -1
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/springBoot.cjs.map +1 -1
- package/dist/presets/springBoot.js.map +1 -1
- package/dist/presets/stdio.cjs.map +1 -1
- package/dist/presets/stdio.js.map +1 -1
- package/dist/presets/tomcat.cjs.map +1 -1
- package/dist/presets/tomcat.js.map +1 -1
- package/dist/types/decisionCode.cjs.map +1 -1
- package/dist/types/decisionCode.js.map +1 -1
- package/dist/types/language.cjs.map +1 -1
- package/dist/types/language.js.map +1 -1
- package/dist/types/problem.cjs.map +1 -1
- package/dist/types/problem.js.map +1 -1
- package/dist/types/testCaseResult.cjs.map +1 -1
- package/dist/types/testCaseResult.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guiCommand.cjs","names":["os","z","parseArgs","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","getErrorMessage","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","path","languageIdToDefinition","spawnWithTimeout","readFileTestCases","childProcess","GuiRecorder","wait","fs"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"guiCommand.cjs","names":["os","z","parseArgs","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","getErrorMessage","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","path","languageIdToDefinition","spawnWithTimeout","readFileTestCases","childProcess","GuiRecorder","wait","fs","require_runtime","require_printTestCaseResult","require_parseArgs","require_readTestCases","require_language","require_findLanguageDefinitionByPath","require_readProblemMarkdownFrontMatter","require_decisionCode","require_guiRecording","require_cleanWorkingDirectory","require_copyTestCaseFileInput","require_findEntryPointFile","require_judgeByStaticAnalysis","require_readOutputFiles","require_spawnWithTimeout","node_path","zod","node_fs_promises","node_os","_willbooster_shared_lib","node_child_process","node_timers_promises","TIME_COMMAND","judgeParamsSchema","guiCommandJudgePreset","args","params","submissionDir","problemMarkdownFrontMatter","problemDir","configuredTestCases","options","readGuiTestCases","testCases","prebuildTestCaseId","staticAnalysisResult","initialMainFilePath","languageDefinition","env","resolvedMainFilePath","resolveMainFilePath","prebuiltMainFilePath","inferLanguageIdsByPath","error","customPrepareResult","prepareResult","runDefaultPrepare","cwdSnapshot","displayServer","currentTestCaseId","currentStdin","ensureDisplayServer","sharedFileInputPath","testCase","timeLimitSeconds","runEnv","stdin","command","runResult","runContext","result","spawnGuiProgram","outputFiles","judgeContext","baseJudgeResult","evaluateGuiRunResult","judgeResult","extendedJudgeResult","decisionCode","stdout","stderr","context","pathExists","languageIds","definition","filePath","ext","languageId","buildCommand","buildResult","buildOutput","requiredOutputFilesCount","child","exitCode","spawnError","stopReason","screenshotSignaturesHistory","screenshots","recorder","startTimeSeconds","sampledMemoryBytes","chunk","code","signal","captureSeconds","nextCaptureEndSeconds","readProcessGroupMemoryBytes","captureStartMs","capturedWindows","takeScreenshots","capturedAtMs","isSinglePixel","windowId","screenshot","a","b","screenshotSignatures","file","files","index","stopProcess","normalizedStderr","parseTimedStderr","display","xwininfo","extractTopLevelWindows","windowNameResult","windowName","windows","lines","line","match","parsedMemoryBytes","displayNumber","xvfb","killProcessGroup","processGroupId","value","sum"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guiCommand.js","names":["readFileTestCases","wait"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"guiCommand.js","names":["readFileTestCases","wait","path","fs","os","childProcess","TIME_COMMAND","judgeParamsSchema","z","guiCommandJudgePreset","args","parseArgs","params","submissionDir","problemMarkdownFrontMatter","readProblemMarkdownFrontMatter","problemDir","configuredTestCases","options","readGuiTestCases","testCases","prebuildTestCaseId","staticAnalysisResult","judgeByStaticAnalysis","printTestCaseResult","initialMainFilePath","findEntryPointFile","DecisionCode","languageDefinition","findLanguageDefinitionByPath","env","resolvedMainFilePath","resolveMainFilePath","prebuiltMainFilePath","inferLanguageIdsByPath","error","getErrorMessage","customPrepareResult","prepareResult","runDefaultPrepare","cwdSnapshot","snapshotWorkingDirectory","displayServer","currentTestCaseId","currentStdin","ensureDisplayServer","sharedFileInputPath","testCase","copyTestCaseFileInput","timeLimitSeconds","runEnv","stdin","command","runResult","runContext","result","spawnGuiProgram","cleanWorkingDirectory","outputFiles","readOutputFiles","judgeContext","baseJudgeResult","evaluateGuiRunResult","judgeResult","extendedJudgeResult","decisionCode","stdout","stderr","context","pathExists","languageIds","languageIdToDefinition","definition","filePath","ext","languageId","buildCommand","buildResult","spawnWithTimeout","buildOutput","requiredOutputFilesCount","readTestCases","child","exitCode","spawnError","stopReason","screenshotSignaturesHistory","screenshots","recorder","GuiRecorder","startTimeSeconds","sampledMemoryBytes","chunk","code","signal","captureSeconds","nextCaptureEndSeconds","setTimeout","readProcessGroupMemoryBytes","captureStartMs","capturedWindows","takeScreenshots","capturedAtMs","isSinglePixel","windowId","screenshot","a","b","screenshotSignatures","file","files","index","stopProcess","normalizedStderr","parseTimedStderr","display","xwininfo","extractTopLevelWindows","windowNameResult","windowName","windows","lines","line","match","parsedMemoryBytes","displayNumber","xvfb","killProcessGroup","processGroupId","value","sum"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"springBoot.cjs","names":["getErrorMessage","parseArgs","fs","path","tmpdir","DecisionCode","listSubmissionFiles","resolveJavaRelativePath","spawnSync","MAX_OUTPUT_LENGTH","truncateOutput","hasErrorCode","spawn","sleep","readOutputFiles"],"sources":["../../src/presets/springBoot.ts"],"sourcesContent":["import {\n MAX_OUTPUT_LENGTH,\n resolveJavaRelativePath,\n listSubmissionFiles,\n readOutputFiles,\n truncateOutput,\n} from '../helpers/javaJudge.js';\nimport { type ChildProcess, spawn, spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { getErrorMessage, hasErrorCode } from '@willbooster/shared-lib';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nexport interface SpringBootJudgePresetOptions {\n problemDirectoryPath: string;\n evaluate: () => Promise<void>;\n}\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\nconst APPLICATION_NAME = 'judge';\n\nconst SERVER_PORT = 59_000;\n\nconst SERVER_BASE_URL = `http://localhost:${SERVER_PORT}`;\n\nconst BUILD_TIMEOUT_MS = 90_000;\n\nconst STARTUP_TIMEOUT_MS = 60_000;\n\nconst EVALUATE_TIMEOUT_MS = 60_000;\n\nexport async function springBootJudgePreset(options: SpringBootJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(getErrorMessage(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'spring_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let springBootProcess: ChildProcess | undefined;\n\n try {\n await prepareBuildDirectory(buildDir, submissionDir, options.problemDirectoryPath);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n const jarPath = path.join(buildDir, 'target', `${APPLICATION_NAME}.jar`);\n const startResult = await startSpringBootApplication(buildDir, jarPath);\n springBootProcess = startResult.childProcess;\n if (startResult.failure) {\n printTestCaseResult({ testCaseId, ...startResult.failure });\n return;\n }\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: getErrorMessage(error),\n });\n } finally {\n await stopSpringBootApplication(springBootProcess);\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n problemDirectoryPath: string\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/resources'), { recursive: true });\n await fs.promises.copyFile(path.join(problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n await fs.promises.writeFile(\n path.join(buildDir, 'src/main/resources/application.properties'),\n createApplicationProperties()\n );\n\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nfunction createApplicationProperties(): string {\n return `spring.application.name=${APPLICATION_NAME}\nserver.port=${SERVER_PORT}\nspring.main.banner-mode=off\nlogging.level.org.springframework=ERROR\nlogging.level.org.apache=ERROR\nspring.main.log-startup-info=false\n`;\n}\n\nasync function resolveTargetPath(buildDir: string, relativePath: string, sourcePath: string): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (path.extname(relativePath).toLowerCase() === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n\n return path.join(buildDir, 'src/main/resources', normalizedRelativePath);\n}\n\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const startedAt = Date.now();\n const result = spawnSync('mvn', ['clean', 'package', '--quiet', '--batch-mode', '--offline', '-DskipTests'], {\n cwd: buildDir,\n encoding: 'utf8',\n env: { ...process.env, CI: '', FORCE_COLOR: '0' },\n timeout: BUILD_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds };\n }\n if ((result.status ?? 0) === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus: result.status ?? undefined,\n stderr: truncateOutput(extractMavenErrors(stdout) || stderr),\n stdout,\n timeSeconds,\n };\n}\n\nfunction extractMavenErrors(output: string): string {\n const plainOutput = output.replaceAll(/\u001b\\[\\d*(?:;\\d+)*m/gu, '');\n const errorMessages = new Set<string>();\n for (const match of plainOutput.matchAll(/\\].*?\\[\\d+,\\d+\\]\\s+(.*?)$/gmu)) {\n const message = match[1]!.trim();\n if (message) {\n errorMessages.add(message);\n }\n }\n return errorMessages.size > 0 ? `ビルドエラー:\\n${[...errorMessages].join('\\n')}` : '';\n}\n\nasync function startSpringBootApplication(\n buildDir: string,\n jarPath: string\n): Promise<{ childProcess: ChildProcess; failure?: JudgeResult }> {\n const startedAt = Date.now();\n const childProcess = spawn('java', ['-jar', jarPath], {\n cwd: buildDir,\n env: { ...process.env, SPRING_MAIN_WEB_APPLICATION_TYPE: 'servlet' },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let applicationStdout = '';\n let applicationStderr = '';\n childProcess.stdout?.on('data', (chunk: Buffer) => {\n applicationStdout = truncateOutput(applicationStdout + chunk.toString('utf8'));\n });\n childProcess.stderr?.on('data', (chunk: Buffer) => {\n applicationStderr = truncateOutput(applicationStderr + chunk.toString('utf8'));\n });\n let hasExited = false;\n childProcess.on('exit', () => {\n hasExited = true;\n });\n\n while (Date.now() - startedAt < STARTUP_TIMEOUT_MS) {\n if (await isServerResponding()) {\n return { childProcess };\n }\n if (hasExited) {\n break;\n }\n await sleep(200);\n }\n\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const feedbackMarkdown = hasExited\n ? 'アプリケーションの起動に失敗しました。コンソールのエラーメッセージを確認してください。'\n : `アプリケーションが ${STARTUP_TIMEOUT_MS / 1000} 秒以内に起動しませんでした。`;\n return {\n childProcess,\n failure: {\n decisionCode: hasExited ? DecisionCode.RUNTIME_ERROR : DecisionCode.TIME_LIMIT_EXCEEDED,\n exitStatus: childProcess.exitCode ?? undefined,\n feedbackMarkdown,\n stderr: applicationStderr || undefined,\n stdout: applicationStdout || undefined,\n timeSeconds,\n },\n };\n}\n\nasync function isServerResponding(): Promise<boolean> {\n try {\n // Accept 404 so applications without a root endpoint can finish startup.\n const response = await fetch(SERVER_BASE_URL, { signal: AbortSignal.timeout(1000) });\n await response.body?.cancel();\n return response.ok || response.status === 404;\n } catch {\n return false;\n }\n}\n\nasync function stopSpringBootApplication(childProcess: ChildProcess | undefined): Promise<void> {\n if (!childProcess || childProcess.exitCode !== null) {\n return;\n }\n\n childProcess.kill('SIGTERM');\n const startedAt = Date.now();\n while (Date.now() - startedAt < 5000 && childProcess.exitCode === null) {\n await sleep(100);\n }\n if (childProcess.exitCode === null) {\n childProcess.kill('SIGKILL');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const startedAt = Date.now();\n const result = spawnSync('bun', ['run', './judge.ts', '--evaluate'], {\n cwd: problemDirectoryPath,\n encoding: 'utf8',\n env: { ...process.env, WORKING_DIRECTORY_PATH: buildDir },\n timeout: EVALUATE_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds, outputFiles };\n }\n if ((result.status ?? 0) !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n exitStatus: result.status ?? undefined,\n stderr,\n stdout,\n timeSeconds,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n exitStatus: result.status ?? undefined,\n stderr: stderr || undefined,\n stdout: stdout || undefined,\n timeSeconds,\n outputFiles,\n };\n}\n\nexport function buildSpringBootUrl(urlPath: string): string {\n return `${SERVER_BASE_URL}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"springBoot.cjs","names":["getErrorMessage","parseArgs","fs","path","tmpdir","DecisionCode","listSubmissionFiles","resolveJavaRelativePath","spawnSync","MAX_OUTPUT_LENGTH","truncateOutput","hasErrorCode","spawn","sleep","readOutputFiles","require_runtime","require_printTestCaseResult","require_parseArgs","require_decisionCode","require_javaJudge","node_fs","node_path","node_os","_willbooster_shared_lib","node_child_process","node_timers_promises","APPLICATION_NAME","SERVER_PORT","SERVER_BASE_URL","STARTUP_TIMEOUT_MS","springBootJudgePreset","options","error","submissionDir","testCaseId","runRootDir","buildDir","springBootProcess","prepareBuildDirectory","buildResult","buildWithMaven","startResult","startSpringBootApplication","judgeResult","runJudgeScript","stopSpringBootApplication","problemDirectoryPath","createApplicationProperties","relativePath","sourcePath","targetPath","resolveTargetPath","normalizedRelativePath","startedAt","result","timeSeconds","stdout","stderr","extractMavenErrors","plainOutput","output","errorMessages","match","message","childProcess","jarPath","applicationStdout","applicationStderr","chunk","hasExited","isServerResponding","feedbackMarkdown","response","outputFiles","buildSpringBootUrl","urlPath"],"sources":["../../src/presets/springBoot.ts"],"sourcesContent":["import {\n MAX_OUTPUT_LENGTH,\n resolveJavaRelativePath,\n listSubmissionFiles,\n readOutputFiles,\n truncateOutput,\n} from '../helpers/javaJudge.js';\nimport { type ChildProcess, spawn, spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { getErrorMessage, hasErrorCode } from '@willbooster/shared-lib';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nexport interface SpringBootJudgePresetOptions {\n problemDirectoryPath: string;\n evaluate: () => Promise<void>;\n}\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\nconst APPLICATION_NAME = 'judge';\n\nconst SERVER_PORT = 59_000;\n\nconst SERVER_BASE_URL = `http://localhost:${SERVER_PORT}`;\n\nconst BUILD_TIMEOUT_MS = 90_000;\n\nconst STARTUP_TIMEOUT_MS = 60_000;\n\nconst EVALUATE_TIMEOUT_MS = 60_000;\n\nexport async function springBootJudgePreset(options: SpringBootJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(getErrorMessage(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'spring_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let springBootProcess: ChildProcess | undefined;\n\n try {\n await prepareBuildDirectory(buildDir, submissionDir, options.problemDirectoryPath);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n const jarPath = path.join(buildDir, 'target', `${APPLICATION_NAME}.jar`);\n const startResult = await startSpringBootApplication(buildDir, jarPath);\n springBootProcess = startResult.childProcess;\n if (startResult.failure) {\n printTestCaseResult({ testCaseId, ...startResult.failure });\n return;\n }\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: getErrorMessage(error),\n });\n } finally {\n await stopSpringBootApplication(springBootProcess);\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n problemDirectoryPath: string\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/resources'), { recursive: true });\n await fs.promises.copyFile(path.join(problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n await fs.promises.writeFile(\n path.join(buildDir, 'src/main/resources/application.properties'),\n createApplicationProperties()\n );\n\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nfunction createApplicationProperties(): string {\n return `spring.application.name=${APPLICATION_NAME}\nserver.port=${SERVER_PORT}\nspring.main.banner-mode=off\nlogging.level.org.springframework=ERROR\nlogging.level.org.apache=ERROR\nspring.main.log-startup-info=false\n`;\n}\n\nasync function resolveTargetPath(buildDir: string, relativePath: string, sourcePath: string): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (path.extname(relativePath).toLowerCase() === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n\n return path.join(buildDir, 'src/main/resources', normalizedRelativePath);\n}\n\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const startedAt = Date.now();\n const result = spawnSync('mvn', ['clean', 'package', '--quiet', '--batch-mode', '--offline', '-DskipTests'], {\n cwd: buildDir,\n encoding: 'utf8',\n env: { ...process.env, CI: '', FORCE_COLOR: '0' },\n timeout: BUILD_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds };\n }\n if ((result.status ?? 0) === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus: result.status ?? undefined,\n stderr: truncateOutput(extractMavenErrors(stdout) || stderr),\n stdout,\n timeSeconds,\n };\n}\n\nfunction extractMavenErrors(output: string): string {\n const plainOutput = output.replaceAll(/\u001b\\[\\d*(?:;\\d+)*m/gu, '');\n const errorMessages = new Set<string>();\n for (const match of plainOutput.matchAll(/\\].*?\\[\\d+,\\d+\\]\\s+(.*?)$/gmu)) {\n const message = match[1]!.trim();\n if (message) {\n errorMessages.add(message);\n }\n }\n return errorMessages.size > 0 ? `ビルドエラー:\\n${[...errorMessages].join('\\n')}` : '';\n}\n\nasync function startSpringBootApplication(\n buildDir: string,\n jarPath: string\n): Promise<{ childProcess: ChildProcess; failure?: JudgeResult }> {\n const startedAt = Date.now();\n const childProcess = spawn('java', ['-jar', jarPath], {\n cwd: buildDir,\n env: { ...process.env, SPRING_MAIN_WEB_APPLICATION_TYPE: 'servlet' },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let applicationStdout = '';\n let applicationStderr = '';\n childProcess.stdout?.on('data', (chunk: Buffer) => {\n applicationStdout = truncateOutput(applicationStdout + chunk.toString('utf8'));\n });\n childProcess.stderr?.on('data', (chunk: Buffer) => {\n applicationStderr = truncateOutput(applicationStderr + chunk.toString('utf8'));\n });\n let hasExited = false;\n childProcess.on('exit', () => {\n hasExited = true;\n });\n\n while (Date.now() - startedAt < STARTUP_TIMEOUT_MS) {\n if (await isServerResponding()) {\n return { childProcess };\n }\n if (hasExited) {\n break;\n }\n await sleep(200);\n }\n\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const feedbackMarkdown = hasExited\n ? 'アプリケーションの起動に失敗しました。コンソールのエラーメッセージを確認してください。'\n : `アプリケーションが ${STARTUP_TIMEOUT_MS / 1000} 秒以内に起動しませんでした。`;\n return {\n childProcess,\n failure: {\n decisionCode: hasExited ? DecisionCode.RUNTIME_ERROR : DecisionCode.TIME_LIMIT_EXCEEDED,\n exitStatus: childProcess.exitCode ?? undefined,\n feedbackMarkdown,\n stderr: applicationStderr || undefined,\n stdout: applicationStdout || undefined,\n timeSeconds,\n },\n };\n}\n\nasync function isServerResponding(): Promise<boolean> {\n try {\n // Accept 404 so applications without a root endpoint can finish startup.\n const response = await fetch(SERVER_BASE_URL, { signal: AbortSignal.timeout(1000) });\n await response.body?.cancel();\n return response.ok || response.status === 404;\n } catch {\n return false;\n }\n}\n\nasync function stopSpringBootApplication(childProcess: ChildProcess | undefined): Promise<void> {\n if (!childProcess || childProcess.exitCode !== null) {\n return;\n }\n\n childProcess.kill('SIGTERM');\n const startedAt = Date.now();\n while (Date.now() - startedAt < 5000 && childProcess.exitCode === null) {\n await sleep(100);\n }\n if (childProcess.exitCode === null) {\n childProcess.kill('SIGKILL');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const startedAt = Date.now();\n const result = spawnSync('bun', ['run', './judge.ts', '--evaluate'], {\n cwd: problemDirectoryPath,\n encoding: 'utf8',\n env: { ...process.env, WORKING_DIRECTORY_PATH: buildDir },\n timeout: EVALUATE_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds, outputFiles };\n }\n if ((result.status ?? 0) !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n exitStatus: result.status ?? undefined,\n stderr,\n stdout,\n timeSeconds,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n exitStatus: result.status ?? undefined,\n stderr: stderr || undefined,\n stdout: stdout || undefined,\n timeSeconds,\n outputFiles,\n };\n}\n\nexport function buildSpringBootUrl(urlPath: string): string {\n return `${SERVER_BASE_URL}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"springBoot.js","names":["sleep"],"sources":["../../src/presets/springBoot.ts"],"sourcesContent":["import {\n MAX_OUTPUT_LENGTH,\n resolveJavaRelativePath,\n listSubmissionFiles,\n readOutputFiles,\n truncateOutput,\n} from '../helpers/javaJudge.js';\nimport { type ChildProcess, spawn, spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { getErrorMessage, hasErrorCode } from '@willbooster/shared-lib';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nexport interface SpringBootJudgePresetOptions {\n problemDirectoryPath: string;\n evaluate: () => Promise<void>;\n}\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\nconst APPLICATION_NAME = 'judge';\n\nconst SERVER_PORT = 59_000;\n\nconst SERVER_BASE_URL = `http://localhost:${SERVER_PORT}`;\n\nconst BUILD_TIMEOUT_MS = 90_000;\n\nconst STARTUP_TIMEOUT_MS = 60_000;\n\nconst EVALUATE_TIMEOUT_MS = 60_000;\n\nexport async function springBootJudgePreset(options: SpringBootJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(getErrorMessage(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'spring_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let springBootProcess: ChildProcess | undefined;\n\n try {\n await prepareBuildDirectory(buildDir, submissionDir, options.problemDirectoryPath);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n const jarPath = path.join(buildDir, 'target', `${APPLICATION_NAME}.jar`);\n const startResult = await startSpringBootApplication(buildDir, jarPath);\n springBootProcess = startResult.childProcess;\n if (startResult.failure) {\n printTestCaseResult({ testCaseId, ...startResult.failure });\n return;\n }\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: getErrorMessage(error),\n });\n } finally {\n await stopSpringBootApplication(springBootProcess);\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n problemDirectoryPath: string\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/resources'), { recursive: true });\n await fs.promises.copyFile(path.join(problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n await fs.promises.writeFile(\n path.join(buildDir, 'src/main/resources/application.properties'),\n createApplicationProperties()\n );\n\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nfunction createApplicationProperties(): string {\n return `spring.application.name=${APPLICATION_NAME}\nserver.port=${SERVER_PORT}\nspring.main.banner-mode=off\nlogging.level.org.springframework=ERROR\nlogging.level.org.apache=ERROR\nspring.main.log-startup-info=false\n`;\n}\n\nasync function resolveTargetPath(buildDir: string, relativePath: string, sourcePath: string): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (path.extname(relativePath).toLowerCase() === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n\n return path.join(buildDir, 'src/main/resources', normalizedRelativePath);\n}\n\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const startedAt = Date.now();\n const result = spawnSync('mvn', ['clean', 'package', '--quiet', '--batch-mode', '--offline', '-DskipTests'], {\n cwd: buildDir,\n encoding: 'utf8',\n env: { ...process.env, CI: '', FORCE_COLOR: '0' },\n timeout: BUILD_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds };\n }\n if ((result.status ?? 0) === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus: result.status ?? undefined,\n stderr: truncateOutput(extractMavenErrors(stdout) || stderr),\n stdout,\n timeSeconds,\n };\n}\n\nfunction extractMavenErrors(output: string): string {\n const plainOutput = output.replaceAll(/\u001b\\[\\d*(?:;\\d+)*m/gu, '');\n const errorMessages = new Set<string>();\n for (const match of plainOutput.matchAll(/\\].*?\\[\\d+,\\d+\\]\\s+(.*?)$/gmu)) {\n const message = match[1]!.trim();\n if (message) {\n errorMessages.add(message);\n }\n }\n return errorMessages.size > 0 ? `ビルドエラー:\\n${[...errorMessages].join('\\n')}` : '';\n}\n\nasync function startSpringBootApplication(\n buildDir: string,\n jarPath: string\n): Promise<{ childProcess: ChildProcess; failure?: JudgeResult }> {\n const startedAt = Date.now();\n const childProcess = spawn('java', ['-jar', jarPath], {\n cwd: buildDir,\n env: { ...process.env, SPRING_MAIN_WEB_APPLICATION_TYPE: 'servlet' },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let applicationStdout = '';\n let applicationStderr = '';\n childProcess.stdout?.on('data', (chunk: Buffer) => {\n applicationStdout = truncateOutput(applicationStdout + chunk.toString('utf8'));\n });\n childProcess.stderr?.on('data', (chunk: Buffer) => {\n applicationStderr = truncateOutput(applicationStderr + chunk.toString('utf8'));\n });\n let hasExited = false;\n childProcess.on('exit', () => {\n hasExited = true;\n });\n\n while (Date.now() - startedAt < STARTUP_TIMEOUT_MS) {\n if (await isServerResponding()) {\n return { childProcess };\n }\n if (hasExited) {\n break;\n }\n await sleep(200);\n }\n\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const feedbackMarkdown = hasExited\n ? 'アプリケーションの起動に失敗しました。コンソールのエラーメッセージを確認してください。'\n : `アプリケーションが ${STARTUP_TIMEOUT_MS / 1000} 秒以内に起動しませんでした。`;\n return {\n childProcess,\n failure: {\n decisionCode: hasExited ? DecisionCode.RUNTIME_ERROR : DecisionCode.TIME_LIMIT_EXCEEDED,\n exitStatus: childProcess.exitCode ?? undefined,\n feedbackMarkdown,\n stderr: applicationStderr || undefined,\n stdout: applicationStdout || undefined,\n timeSeconds,\n },\n };\n}\n\nasync function isServerResponding(): Promise<boolean> {\n try {\n // Accept 404 so applications without a root endpoint can finish startup.\n const response = await fetch(SERVER_BASE_URL, { signal: AbortSignal.timeout(1000) });\n await response.body?.cancel();\n return response.ok || response.status === 404;\n } catch {\n return false;\n }\n}\n\nasync function stopSpringBootApplication(childProcess: ChildProcess | undefined): Promise<void> {\n if (!childProcess || childProcess.exitCode !== null) {\n return;\n }\n\n childProcess.kill('SIGTERM');\n const startedAt = Date.now();\n while (Date.now() - startedAt < 5000 && childProcess.exitCode === null) {\n await sleep(100);\n }\n if (childProcess.exitCode === null) {\n childProcess.kill('SIGKILL');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const startedAt = Date.now();\n const result = spawnSync('bun', ['run', './judge.ts', '--evaluate'], {\n cwd: problemDirectoryPath,\n encoding: 'utf8',\n env: { ...process.env, WORKING_DIRECTORY_PATH: buildDir },\n timeout: EVALUATE_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds, outputFiles };\n }\n if ((result.status ?? 0) !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n exitStatus: result.status ?? undefined,\n stderr,\n stdout,\n timeSeconds,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n exitStatus: result.status ?? undefined,\n stderr: stderr || undefined,\n stdout: stdout || undefined,\n timeSeconds,\n outputFiles,\n };\n}\n\nexport function buildSpringBootUrl(urlPath: string): string {\n return `${SERVER_BASE_URL}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"springBoot.js","names":["sleep","fs","path","APPLICATION_NAME","SERVER_PORT","SERVER_BASE_URL","STARTUP_TIMEOUT_MS","springBootJudgePreset","options","error","getErrorMessage","submissionDir","parseArgs","testCaseId","runRootDir","tmpdir","buildDir","springBootProcess","prepareBuildDirectory","buildResult","buildWithMaven","printTestCaseResult","startResult","startSpringBootApplication","judgeResult","runJudgeScript","DecisionCode","stopSpringBootApplication","problemDirectoryPath","createApplicationProperties","relativePath","listSubmissionFiles","sourcePath","targetPath","resolveTargetPath","normalizedRelativePath","resolveJavaRelativePath","startedAt","result","spawnSync","MAX_OUTPUT_LENGTH","timeSeconds","stdout","truncateOutput","stderr","hasErrorCode","extractMavenErrors","plainOutput","output","errorMessages","match","message","childProcess","spawn","jarPath","applicationStdout","applicationStderr","chunk","hasExited","isServerResponding","setTimeout","feedbackMarkdown","response","outputFiles","readOutputFiles","buildSpringBootUrl","urlPath"],"sources":["../../src/presets/springBoot.ts"],"sourcesContent":["import {\n MAX_OUTPUT_LENGTH,\n resolveJavaRelativePath,\n listSubmissionFiles,\n readOutputFiles,\n truncateOutput,\n} from '../helpers/javaJudge.js';\nimport { type ChildProcess, spawn, spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { getErrorMessage, hasErrorCode } from '@willbooster/shared-lib';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nexport interface SpringBootJudgePresetOptions {\n problemDirectoryPath: string;\n evaluate: () => Promise<void>;\n}\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\nconst APPLICATION_NAME = 'judge';\n\nconst SERVER_PORT = 59_000;\n\nconst SERVER_BASE_URL = `http://localhost:${SERVER_PORT}`;\n\nconst BUILD_TIMEOUT_MS = 90_000;\n\nconst STARTUP_TIMEOUT_MS = 60_000;\n\nconst EVALUATE_TIMEOUT_MS = 60_000;\n\nexport async function springBootJudgePreset(options: SpringBootJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(getErrorMessage(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'spring_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let springBootProcess: ChildProcess | undefined;\n\n try {\n await prepareBuildDirectory(buildDir, submissionDir, options.problemDirectoryPath);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n const jarPath = path.join(buildDir, 'target', `${APPLICATION_NAME}.jar`);\n const startResult = await startSpringBootApplication(buildDir, jarPath);\n springBootProcess = startResult.childProcess;\n if (startResult.failure) {\n printTestCaseResult({ testCaseId, ...startResult.failure });\n return;\n }\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: getErrorMessage(error),\n });\n } finally {\n await stopSpringBootApplication(springBootProcess);\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n problemDirectoryPath: string\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/resources'), { recursive: true });\n await fs.promises.copyFile(path.join(problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n await fs.promises.writeFile(\n path.join(buildDir, 'src/main/resources/application.properties'),\n createApplicationProperties()\n );\n\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nfunction createApplicationProperties(): string {\n return `spring.application.name=${APPLICATION_NAME}\nserver.port=${SERVER_PORT}\nspring.main.banner-mode=off\nlogging.level.org.springframework=ERROR\nlogging.level.org.apache=ERROR\nspring.main.log-startup-info=false\n`;\n}\n\nasync function resolveTargetPath(buildDir: string, relativePath: string, sourcePath: string): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (path.extname(relativePath).toLowerCase() === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n\n return path.join(buildDir, 'src/main/resources', normalizedRelativePath);\n}\n\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const startedAt = Date.now();\n const result = spawnSync('mvn', ['clean', 'package', '--quiet', '--batch-mode', '--offline', '-DskipTests'], {\n cwd: buildDir,\n encoding: 'utf8',\n env: { ...process.env, CI: '', FORCE_COLOR: '0' },\n timeout: BUILD_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds };\n }\n if ((result.status ?? 0) === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus: result.status ?? undefined,\n stderr: truncateOutput(extractMavenErrors(stdout) || stderr),\n stdout,\n timeSeconds,\n };\n}\n\nfunction extractMavenErrors(output: string): string {\n const plainOutput = output.replaceAll(/\u001b\\[\\d*(?:;\\d+)*m/gu, '');\n const errorMessages = new Set<string>();\n for (const match of plainOutput.matchAll(/\\].*?\\[\\d+,\\d+\\]\\s+(.*?)$/gmu)) {\n const message = match[1]!.trim();\n if (message) {\n errorMessages.add(message);\n }\n }\n return errorMessages.size > 0 ? `ビルドエラー:\\n${[...errorMessages].join('\\n')}` : '';\n}\n\nasync function startSpringBootApplication(\n buildDir: string,\n jarPath: string\n): Promise<{ childProcess: ChildProcess; failure?: JudgeResult }> {\n const startedAt = Date.now();\n const childProcess = spawn('java', ['-jar', jarPath], {\n cwd: buildDir,\n env: { ...process.env, SPRING_MAIN_WEB_APPLICATION_TYPE: 'servlet' },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let applicationStdout = '';\n let applicationStderr = '';\n childProcess.stdout?.on('data', (chunk: Buffer) => {\n applicationStdout = truncateOutput(applicationStdout + chunk.toString('utf8'));\n });\n childProcess.stderr?.on('data', (chunk: Buffer) => {\n applicationStderr = truncateOutput(applicationStderr + chunk.toString('utf8'));\n });\n let hasExited = false;\n childProcess.on('exit', () => {\n hasExited = true;\n });\n\n while (Date.now() - startedAt < STARTUP_TIMEOUT_MS) {\n if (await isServerResponding()) {\n return { childProcess };\n }\n if (hasExited) {\n break;\n }\n await sleep(200);\n }\n\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const feedbackMarkdown = hasExited\n ? 'アプリケーションの起動に失敗しました。コンソールのエラーメッセージを確認してください。'\n : `アプリケーションが ${STARTUP_TIMEOUT_MS / 1000} 秒以内に起動しませんでした。`;\n return {\n childProcess,\n failure: {\n decisionCode: hasExited ? DecisionCode.RUNTIME_ERROR : DecisionCode.TIME_LIMIT_EXCEEDED,\n exitStatus: childProcess.exitCode ?? undefined,\n feedbackMarkdown,\n stderr: applicationStderr || undefined,\n stdout: applicationStdout || undefined,\n timeSeconds,\n },\n };\n}\n\nasync function isServerResponding(): Promise<boolean> {\n try {\n // Accept 404 so applications without a root endpoint can finish startup.\n const response = await fetch(SERVER_BASE_URL, { signal: AbortSignal.timeout(1000) });\n await response.body?.cancel();\n return response.ok || response.status === 404;\n } catch {\n return false;\n }\n}\n\nasync function stopSpringBootApplication(childProcess: ChildProcess | undefined): Promise<void> {\n if (!childProcess || childProcess.exitCode !== null) {\n return;\n }\n\n childProcess.kill('SIGTERM');\n const startedAt = Date.now();\n while (Date.now() - startedAt < 5000 && childProcess.exitCode === null) {\n await sleep(100);\n }\n if (childProcess.exitCode === null) {\n childProcess.kill('SIGKILL');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const startedAt = Date.now();\n const result = spawnSync('bun', ['run', './judge.ts', '--evaluate'], {\n cwd: problemDirectoryPath,\n encoding: 'utf8',\n env: { ...process.env, WORKING_DIRECTORY_PATH: buildDir },\n timeout: EVALUATE_TIMEOUT_MS,\n maxBuffer: MAX_OUTPUT_LENGTH * 4,\n });\n const timeSeconds = (Date.now() - startedAt) / 1000;\n const stdout = truncateOutput(result.stdout ?? '');\n const stderr = truncateOutput(result.stderr ?? '');\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n\n if (hasErrorCode(result.error, 'ETIMEDOUT')) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr, stdout, timeSeconds, outputFiles };\n }\n if ((result.status ?? 0) !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n exitStatus: result.status ?? undefined,\n stderr,\n stdout,\n timeSeconds,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n exitStatus: result.status ?? undefined,\n stderr: stderr || undefined,\n stdout: stdout || undefined,\n timeSeconds,\n outputFiles,\n };\n}\n\nexport function buildSpringBootUrl(urlPath: string): string {\n return `${SERVER_BASE_URL}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
|