@exercode/problem-utils 1.17.8 → 1.18.1
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/README.md +1 -1
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
- package/dist/helpers/checkProblemDirIsolation.js +1 -1
- package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.d.ts +6 -2
- package/dist/helpers/cleanWorkingDirectory.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.js +1 -1
- package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
- package/dist/helpers/judgeByStaticAnalysis.cjs +1 -1
- package/dist/helpers/judgeByStaticAnalysis.js +1 -1
- package/dist/helpers/readOutputFiles.cjs +1 -1
- package/dist/helpers/readOutputFiles.cjs.map +1 -1
- package/dist/helpers/readOutputFiles.d.ts +9 -0
- package/dist/helpers/readOutputFiles.js +1 -1
- package/dist/helpers/readOutputFiles.js.map +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.cjs +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.js +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
- package/dist/helpers/readTestCases.cjs +1 -1
- package/dist/helpers/readTestCases.js +1 -1
- package/dist/helpers/resolveCwds.cjs +1 -1
- package/dist/helpers/resolveCwds.cjs.map +1 -1
- package/dist/helpers/resolveCwds.js +1 -1
- package/dist/helpers/resolveCwds.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.d.ts +3 -0
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/runCustomRunner.cjs +2 -0
- package/dist/helpers/runCustomRunner.cjs.map +1 -0
- package/dist/helpers/runCustomRunner.d.ts +8 -0
- package/dist/helpers/runCustomRunner.js +2 -0
- package/dist/helpers/runCustomRunner.js.map +1 -0
- package/dist/helpers/safeFs.cjs +2 -0
- package/dist/helpers/safeFs.cjs.map +1 -0
- package/dist/helpers/safeFs.d.ts +29 -0
- package/dist/helpers/safeFs.js +2 -0
- package/dist/helpers/safeFs.js.map +1 -0
- package/dist/helpers/sandboxUser.cjs +2 -0
- package/dist/helpers/sandboxUser.cjs.map +1 -0
- package/dist/helpers/sandboxUser.d.ts +136 -0
- package/dist/helpers/sandboxUser.js +2 -0
- package/dist/helpers/sandboxUser.js.map +1 -0
- package/dist/helpers/spawnSyncWithTimeout.cjs +1 -1
- package/dist/helpers/spawnSyncWithTimeout.cjs.map +1 -1
- package/dist/helpers/spawnSyncWithTimeout.d.ts +7 -0
- package/dist/helpers/spawnSyncWithTimeout.js +1 -1
- package/dist/helpers/spawnSyncWithTimeout.js.map +1 -1
- package/dist/helpers/startHttpServer.cjs +1 -1
- package/dist/helpers/startHttpServer.cjs.map +1 -1
- package/dist/helpers/startHttpServer.js +1 -1
- package/dist/helpers/startHttpServer.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/presets/command.cjs +1 -1
- package/dist/presets/command.cjs.map +1 -1
- package/dist/presets/command.d.ts +16 -0
- package/dist/presets/command.js +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/guiCommand.cjs +3 -3
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.d.ts +24 -0
- package/dist/presets/guiCommand.js +3 -3
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/llm.cjs +1 -1
- package/dist/presets/llm.cjs.map +1 -1
- package/dist/presets/llm.js +1 -1
- package/dist/presets/stdio.cjs +1 -1
- package/dist/presets/stdio.cjs.map +1 -1
- package/dist/presets/stdio.js +1 -1
- package/dist/presets/stdio.js.map +1 -1
- package/dist/types/language.cjs +2 -2
- package/dist/types/language.cjs.map +1 -1
- package/dist/types/language.js +2 -2
- package/dist/types/language.js.map +1 -1
- package/package.json +13 -13
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name of the environment variable through which a judge server tells problem-utils to run
|
|
3
|
+
* untrusted submitted programs as the given unprivileged OS user via sudo.
|
|
4
|
+
*
|
|
5
|
+
* The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its
|
|
6
|
+
* own trusted user, so that problem files (test cases and the harness itself) stay unreadable to
|
|
7
|
+
* submissions while the submissions themselves run under the sandbox user. When the variable is
|
|
8
|
+
* absent or empty (local development, course authoring, or an all-sandbox judge run), commands run
|
|
9
|
+
* as the current user like before.
|
|
10
|
+
*
|
|
11
|
+
* Contract for the delegating judge server (delegation is Linux-only — sudo user separation and
|
|
12
|
+
* `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):
|
|
13
|
+
* - The harness process environment is forwarded to sandboxed submissions (sudo runs with
|
|
14
|
+
* `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one
|
|
15
|
+
* exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:
|
|
16
|
+
* `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can
|
|
17
|
+
* recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.
|
|
18
|
+
* - sudoers must let the harness user run arbitrary commands as the sandbox user without a
|
|
19
|
+
* password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge
|
|
20
|
+
* stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):
|
|
21
|
+
* e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus
|
|
22
|
+
* `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.
|
|
23
|
+
* - Variables meant for the submitted program rather than for the harness must be passed under
|
|
24
|
+
* {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's
|
|
25
|
+
* environment.
|
|
26
|
+
* - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is
|
|
27
|
+
* shared across sequential requests, so the server is responsible for resetting whatever
|
|
28
|
+
* cross-request persistence there matters to it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SANDBOX_USER_ENV_NAME = "EXERCODE_SANDBOX_USER";
|
|
31
|
+
/**
|
|
32
|
+
* Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).
|
|
33
|
+
* The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed
|
|
34
|
+
* submission can create executables in its persistent home, so resolving these helpers through the
|
|
35
|
+
* inherited `PATH` would hand the submission code execution as the harness user. Node resolves the
|
|
36
|
+
* command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
39
|
+
export declare const sandboxUserName: string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The deadline supervisor. Absolute under delegation: the wrapper's `exec "$0"` resolves it through
|
|
42
|
+
* the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a
|
|
43
|
+
* bare name would let a submission replace the very process that enforces its time limit. The bare
|
|
44
|
+
* name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).
|
|
45
|
+
*/
|
|
46
|
+
export declare const TIMEOUT_COMMAND: string;
|
|
47
|
+
/**
|
|
48
|
+
* Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process
|
|
49
|
+
* creates world-writable, so the harness user can clean it up without privileges. The wrapper also
|
|
50
|
+
* restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`
|
|
51
|
+
* inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an
|
|
52
|
+
* outer timer alone could not stop a runaway submission.
|
|
53
|
+
*/
|
|
54
|
+
export declare function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]];
|
|
55
|
+
/**
|
|
56
|
+
* Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand
|
|
57
|
+
* custom runners a wrapped command, and a runner may forward it to another helper that wraps too;
|
|
58
|
+
* a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,
|
|
59
|
+
* so every test case of such a problem would fail only under delegation.
|
|
60
|
+
*
|
|
61
|
+
* Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived
|
|
62
|
+
* command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,
|
|
63
|
+
* which would run the submission as the trusted harness user.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isSandboxWrappedCommand(command: readonly string[]): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox
|
|
68
|
+
* wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command
|
|
69
|
+
* from the outside would run `innerPrefix` as the trusted harness user while its arguments (such
|
|
70
|
+
* as an output path) point into a sandbox-writable directory, where a submission can plant a
|
|
71
|
+
* symlink and have the harness truncate an arbitrary file it owns.
|
|
72
|
+
*/
|
|
73
|
+
export declare function prependInsideSandboxWrapper(command: readonly [string, ...string[]], innerPrefix: readonly string[]): [string, ...string[]];
|
|
74
|
+
/**
|
|
75
|
+
* Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`
|
|
76
|
+
* smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the
|
|
77
|
+
* environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives
|
|
78
|
+
* the exec; the harness's own value is only the fallback.
|
|
79
|
+
*/
|
|
80
|
+
export declare function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
81
|
+
/**
|
|
82
|
+
* Prefix under which a delegating judge server passes variables that belong to the submitted
|
|
83
|
+
* program alone. Applying a request's variables to the harness itself would let a submission point
|
|
84
|
+
* e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,
|
|
85
|
+
* so the server prefixes them and problem-utils strips the prefix back off here, where the
|
|
86
|
+
* environment of a sandboxed submission is built.
|
|
87
|
+
*/
|
|
88
|
+
export declare const SANDBOX_ENV_PREFIX = "SANDBOX_ENV_";
|
|
89
|
+
/**
|
|
90
|
+
* Make harness-user-created files under the given path readable, and directories writable, for the
|
|
91
|
+
* sandbox user, so sandboxed programs can read their sources and create outputs next to them.
|
|
92
|
+
*/
|
|
93
|
+
export declare function makeAccessibleToSandboxUser(targetPath: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Kill the sandbox user's processes with the given signals. The harness user cannot signal another
|
|
96
|
+
* user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every
|
|
97
|
+
* sandbox process is safe because the judge server handles one request at a time. The default
|
|
98
|
+
* sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period
|
|
99
|
+
* send `['TERM']`, wait, and then send `['KILL']`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function killSandboxUserProcesses(signals?: readonly ('TERM' | 'KILL')[]): void;
|
|
102
|
+
/**
|
|
103
|
+
* Let the sandbox user reopen the permissions of its own files under the given path, for
|
|
104
|
+
* harness-side traversal/cleanup of trees where a sandboxed process restricted permissions
|
|
105
|
+
* (some tools chmod their outputs regardless of umask).
|
|
106
|
+
*/
|
|
107
|
+
export declare function relaxPermissionsAsSandboxUser(targetPath: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the
|
|
110
|
+
* sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.
|
|
111
|
+
*/
|
|
112
|
+
export declare const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;
|
|
113
|
+
/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */
|
|
114
|
+
export interface SandboxTimeoutWatchdog {
|
|
115
|
+
cancel(): void;
|
|
116
|
+
fired(): boolean;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.
|
|
120
|
+
* A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous
|
|
121
|
+
* spawn blocks the harness's event loop, so without this external deadline a submission could run
|
|
122
|
+
* until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of
|
|
123
|
+
* the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.
|
|
124
|
+
*
|
|
125
|
+
* ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps
|
|
126
|
+
* running after the harness exits and would SIGKILL a later request's submission.
|
|
127
|
+
*/
|
|
128
|
+
export declare function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog;
|
|
129
|
+
/**
|
|
130
|
+
* `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose
|
|
131
|
+
* permissions block deletion: let the sandbox user reopen its own files first, then retry. The
|
|
132
|
+
* containing directory is relaxed as well — unlinking an entry needs write permission on its
|
|
133
|
+
* parent, so a submission that chmods a directory it owns would otherwise make everything inside it
|
|
134
|
+
* undeletable.
|
|
135
|
+
*/
|
|
136
|
+
export declare function forceRemove(targetPath: string): Promise<void>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"node:child_process";import t from"node:fs";import n from"node:os";import r from"node:path";const i=`EXERCODE_SANDBOX_USER`,a=`/usr/bin/sudo`,o={PATH:`/usr/local/bin:/usr/bin:/bin`};function s(e){return c?{...e,PATH:o.PATH}:{...process.env,...e}}const c=process.env.EXERCODE_SANDBOX_USER||void 0;if(c&&c===n.userInfo().username)throw Error(`${i} must name a different OS user than the one running the harness (got "${c}"). Leave it unset to run everything as the current user.`);const l=c?`/usr/bin/timeout`:`timeout`;function u(e){return!c||f(e)?[...e]:[...d(c),...e]}function d(e){return[a,`--preserve-env`,`-u`,e,`--`,`sh`,`-c`,`umask 0; if [ -n "$SANDBOX_LD_LIBRARY_PATH" ]; then export LD_LIBRARY_PATH="$SANDBOX_LD_LIBRARY_PATH"; fi; exec "$0" "$@"`]}function f(e){return c?d(c).every((t,n)=>e[n]===t):!1}function p(e,t){if(!f(e))return[...t,...e];let n=d(c).length;return[...e.slice(0,n),...t,...e.slice(n)]}function m(e){if(!c)return{};let t=e??process.env,n=g(t),r=n.LD_LIBRARY_PATH??t.LD_LIBRARY_PATH??process.env.LD_LIBRARY_PATH;return{HOME:`/home/${c}`,...n,...r&&{SANDBOX_LD_LIBRARY_PATH:r}}}const h=`SANDBOX_ENV_`;function g(e){let t={};for(let[n,r]of Object.entries(e)){if(!n.startsWith(`SANDBOX_ENV_`))continue;let e=n.slice(12);e&&(t[e]=r)}return t}function _(t){c&&e.spawnSync(`/bin/chmod`,[`-R`,`a+rwX`,t],{env:o})}function v(e=[`TERM`,`KILL`]){if(c)for(let t of e){let e=S([`pkill`,`-${t}`,`-u`,c]);if(e.error)throw Error(`failed to terminate ${c} processes: ${e.error.message}`)}}function y(e){c&&S([`chmod`,`-R`,`a+rwX`,e])}const b=5;function x(t){if(!c)return{cancel:()=>{},fired:()=>!1};let n=Math.ceil(t)+5,r=e.spawn(`/bin/sh`,[`-c`,`/bin/sleep ${n}; ${a} -u "$1" pkill -KILL -u "$1"`,`sh`,c],{detached:!0,stdio:`ignore`,env:o}),i=!1;r.on(`error`,()=>{i=!0});let s=!1;r.on(`exit`,()=>{s=!0}),r.unref();let l=Date.now(),u=!1;return{cancel:()=>{if(u=!0,!(r.pid===void 0||s))try{process.kill(-r.pid,`SIGKILL`)}catch{}},fired:()=>i||r.pid===void 0||!u&&Date.now()-l>=n*1e3}}function S(t){return e.spawnSync(a,[`-u`,c,...t],{env:o})}async function C(e){try{await t.promises.rm(e,{force:!0,recursive:!0})}catch(n){if(!c)throw n;y(e),S([`chmod`,`a+rwX`,r.dirname(e)]),await t.promises.rm(e,{force:!0,recursive:!0})}}export{h as SANDBOX_ENV_PREFIX,i as SANDBOX_USER_ENV_NAME,b as SANDBOX_WATCHDOG_GRACE_SECONDS,l as TIMEOUT_COMMAND,C as forceRemove,m as getSandboxUserEnvOverrides,s as getTrustedHelperEnv,f as isSandboxWrappedCommand,v as killSandboxUserProcesses,_ as makeAccessibleToSandboxUser,p as prependInsideSandboxWrapper,y as relaxPermissionsAsSandboxUser,c as sandboxUserName,x as startSandboxTimeoutWatchdog,u as wrapCommandWithSandboxUser};
|
|
2
|
+
//# sourceMappingURL=sandboxUser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandboxUser.js","names":[],"sources":["../../src/helpers/sandboxUser.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Name of the environment variable through which a judge server tells problem-utils to run\n * untrusted submitted programs as the given unprivileged OS user via sudo.\n *\n * The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its\n * own trusted user, so that problem files (test cases and the harness itself) stay unreadable to\n * submissions while the submissions themselves run under the sandbox user. When the variable is\n * absent or empty (local development, course authoring, or an all-sandbox judge run), commands run\n * as the current user like before.\n *\n * Contract for the delegating judge server (delegation is Linux-only — sudo user separation and\n * `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):\n * - The harness process environment is forwarded to sandboxed submissions (sudo runs with\n * `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one\n * exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:\n * `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can\n * recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.\n * - sudoers must let the harness user run arbitrary commands as the sandbox user without a\n * password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge\n * stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):\n * e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus\n * `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.\n * - Variables meant for the submitted program rather than for the harness must be passed under\n * {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's\n * environment.\n * - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is\n * shared across sequential requests, so the server is responsible for resetting whatever\n * cross-request persistence there matters to it.\n */\nexport const SANDBOX_USER_ENV_NAME = 'EXERCODE_SANDBOX_USER';\n\n// Absolute paths so a submission-influenced `PATH` cannot redirect binaries that the trusted\n// harness user executes (`sudo` is also setuid and must be the real one).\nconst SUDO_PATH = '/usr/bin/sudo';\nconst CHMOD_PATH = '/bin/chmod';\nconst SLEEP_PATH = '/bin/sleep';\n// Minimal environment for trusted helper processes; never forward the harness environment to them.\nconst MINIMAL_ENV = { PATH: '/usr/local/bin:/usr/bin:/bin' } as const;\n\n/**\n * Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).\n * The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed\n * submission can create executables in its persistent home, so resolving these helpers through the\n * inherited `PATH` would hand the submission code execution as the harness user. Node resolves the\n * command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.\n */\nexport function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n // Without delegation the submission runs as this same user, so there is no boundary to defend and\n // pinning `PATH` would only break setups whose X11/`ps` binaries live elsewhere (e.g. /usr/games,\n // /snap/bin, Nix). Keep the inherited environment there, exactly as before delegation existed.\n if (!sandboxUserName) return { ...process.env, ...extraEnv };\n // `PATH` last: a caller passing a whole environment through would otherwise reinstate the\n // submission-influenced one this function exists to replace.\n return { ...extraEnv, PATH: MINIMAL_ENV.PATH };\n}\n\nexport const sandboxUserName = process.env[SANDBOX_USER_ENV_NAME] || undefined;\n\n// Fail fast on the catastrophic misconfiguration where the sandbox user is the harness's own user:\n// cleanup (`killSandboxUserProcesses`) would then SIGKILL the harness itself on the first run.\nif (sandboxUserName && sandboxUserName === os.userInfo().username) {\n throw new Error(\n `${SANDBOX_USER_ENV_NAME} must name a different OS user than the one running the harness (got \"${sandboxUserName}\"). Leave it unset to run everything as the current user.`\n );\n}\n\n/**\n * The deadline supervisor. Absolute under delegation: the wrapper's `exec \"$0\"` resolves it through\n * the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a\n * bare name would let a submission replace the very process that enforces its time limit. The bare\n * name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).\n */\nexport const TIMEOUT_COMMAND = sandboxUserName ? '/usr/bin/timeout' : 'timeout';\n\n/**\n * Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process\n * creates world-writable, so the harness user can clean it up without privileges. The wrapper also\n * restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`\n * inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an\n * outer timer alone could not stop a runaway submission.\n */\nexport function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]] {\n if (!sandboxUserName) return [...command];\n // Idempotent; see {@link isSandboxWrappedCommand}.\n if (isSandboxWrappedCommand(command)) return [...command];\n return [...buildSandboxWrapperPrefix(sandboxUserName), ...command];\n}\n\nconst SANDBOX_WRAPPER_SCRIPT =\n 'umask 0; if [ -n \"$SANDBOX_LD_LIBRARY_PATH\" ]; then export LD_LIBRARY_PATH=\"$SANDBOX_LD_LIBRARY_PATH\"; fi; exec \"$0\" \"$@\"';\n\nfunction buildSandboxWrapperPrefix(user: string): [string, ...string[]] {\n return [SUDO_PATH, '--preserve-env', '-u', user, '--', 'sh', '-c', SANDBOX_WRAPPER_SCRIPT];\n}\n\n/**\n * Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand\n * custom runners a wrapped command, and a runner may forward it to another helper that wraps too;\n * a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,\n * so every test case of such a problem would fail only under delegation.\n *\n * Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived\n * command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,\n * which would run the submission as the trusted harness user.\n */\nexport function isSandboxWrappedCommand(command: readonly string[]): boolean {\n if (!sandboxUserName) return false;\n const prefix = buildSandboxWrapperPrefix(sandboxUserName);\n return prefix.every((argument, index) => command[index] === argument);\n}\n\n/**\n * Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox\n * wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command\n * from the outside would run `innerPrefix` as the trusted harness user while its arguments (such\n * as an output path) point into a sandbox-writable directory, where a submission can plant a\n * symlink and have the harness truncate an arbitrary file it owns.\n */\nexport function prependInsideSandboxWrapper(\n command: readonly [string, ...string[]],\n innerPrefix: readonly string[]\n): [string, ...string[]] {\n if (!isSandboxWrappedCommand(command)) return [...innerPrefix, ...command] as [string, ...string[]];\n const prefixLength = buildSandboxWrapperPrefix(sandboxUserName as string).length;\n return [...command.slice(0, prefixLength), ...innerPrefix, ...command.slice(prefixLength)] as [string, ...string[]];\n}\n\n/**\n * Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`\n * smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the\n * environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives\n * the exec; the harness's own value is only the fallback.\n */\nexport function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n if (!sandboxUserName) return {};\n const sourceEnv = env ?? process.env;\n const submissionOnlyEnv = unwrapSubmissionOnlyEnv(sourceEnv);\n // A submission-only `LD_LIBRARY_PATH` must be the one smuggled past the exec, not the harness's.\n const ldLibraryPath = submissionOnlyEnv.LD_LIBRARY_PATH ?? sourceEnv.LD_LIBRARY_PATH ?? process.env.LD_LIBRARY_PATH;\n return {\n HOME: `/home/${sandboxUserName}`,\n ...submissionOnlyEnv,\n // Last: the wrapper reads this one, so a caller must not be able to overwrite it.\n ...(ldLibraryPath && { SANDBOX_LD_LIBRARY_PATH: ldLibraryPath }),\n };\n}\n\n/**\n * Prefix under which a delegating judge server passes variables that belong to the submitted\n * program alone. Applying a request's variables to the harness itself would let a submission point\n * e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,\n * so the server prefixes them and problem-utils strips the prefix back off here, where the\n * environment of a sandboxed submission is built.\n */\nexport const SANDBOX_ENV_PREFIX = 'SANDBOX_ENV_';\n\nfunction unwrapSubmissionOnlyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const unwrapped: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(env)) {\n if (!name.startsWith(SANDBOX_ENV_PREFIX)) continue;\n const unwrappedName = name.slice(SANDBOX_ENV_PREFIX.length);\n if (unwrappedName) unwrapped[unwrappedName] = value;\n }\n return unwrapped;\n}\n\n/**\n * Make harness-user-created files under the given path readable, and directories writable, for the\n * sandbox user, so sandboxed programs can read their sources and create outputs next to them.\n */\nexport function makeAccessibleToSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n // Files owned by the sandbox user fail to chmod but are already world-writable (umask 0).\n child_process.spawnSync(CHMOD_PATH, ['-R', 'a+rwX', targetPath], { env: MINIMAL_ENV });\n}\n\n/**\n * Kill the sandbox user's processes with the given signals. The harness user cannot signal another\n * user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every\n * sandbox process is safe because the judge server handles one request at a time. The default\n * sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period\n * send `['TERM']`, wait, and then send `['KILL']`.\n */\nexport function killSandboxUserProcesses(signals: readonly ('TERM' | 'KILL')[] = ['TERM', 'KILL']): void {\n if (!sandboxUserName) return;\n // One direct call per signal instead of one `sh -c 'pkill ...; pkill ...'`: the wrapping shell\n // would run as the sandbox user too, so the first pkill would kill it before the second ran.\n // Each `pkill` exits with 1 when no process matches, so ignore the exit status.\n for (const signal of signals) {\n const result = runAsSandboxUser(['pkill', `-${signal}`, '-u', sandboxUserName]);\n // Fail closed: a submission can exhaust the PID cgroup so this `sudo` cannot even be spawned.\n // Reporting success would leave its processes alive for the next request on this instance.\n if (result.error) {\n throw new Error(`failed to terminate ${sandboxUserName} processes: ${result.error.message}`);\n }\n }\n}\n\n/**\n * Let the sandbox user reopen the permissions of its own files under the given path, for\n * harness-side traversal/cleanup of trees where a sandboxed process restricted permissions\n * (some tools chmod their outputs regardless of umask).\n */\nexport function relaxPermissionsAsSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n runAsSandboxUser(['chmod', '-R', 'a+rwX', targetPath]);\n}\n\n/**\n * How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the\n * sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.\n */\nexport const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;\n\n/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */\nexport interface SandboxTimeoutWatchdog {\n cancel(): void;\n fired(): boolean;\n}\n\n/**\n * Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.\n * A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous\n * spawn blocks the harness's event loop, so without this external deadline a submission could run\n * until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of\n * the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.\n *\n * ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps\n * running after the harness exits and would SIGKILL a later request's submission.\n */\nexport function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog {\n if (!sandboxUserName) return { cancel: () => {}, fired: () => false };\n\n const deadlineSeconds = Math.ceil(timeoutSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS;\n const watchdog = child_process.spawn(\n '/bin/sh',\n ['-c', `${SLEEP_PATH} ${deadlineSeconds}; ${SUDO_PATH} -u \"$1\" pkill -KILL -u \"$1\"`, 'sh', sandboxUserName],\n { detached: true, stdio: 'ignore', env: MINIMAL_ENV }\n );\n // Without a listener, a spawn failure (e.g. the submission exhausted the PID cgroup) would be an\n // unhandled 'error' event that terminates the harness. Fail closed instead: callers check\n // `fired()`, and a watchdog that never started reports its deadline as elapsed.\n let spawnFailed = false;\n watchdog.on('error', () => {\n spawnFailed = true;\n });\n let exited = false;\n watchdog.on('exit', () => {\n exited = true;\n });\n watchdog.unref();\n\n const startTimeMilliseconds = Date.now();\n let cancelled = false;\n return {\n cancel: () => {\n cancelled = true;\n if (watchdog.pid === undefined || exited) return;\n try {\n process.kill(-watchdog.pid, 'SIGKILL');\n } catch {\n // The watchdog already fired and exited.\n }\n },\n // The `exit`/`error` events cannot be observed from a synchronous caller (they need the event\n // loop), so decide by elapsed time, which is what the watchdog itself waits on.\n fired: () =>\n spawnFailed ||\n watchdog.pid === undefined ||\n (!cancelled && Date.now() - startTimeMilliseconds >= deadlineSeconds * 1000),\n };\n}\n\n/**\n * Run a helper as the sandbox user with a minimal environment. The harness environment must not be\n * passed: sudoers forwards it verbatim, and after sudo drops privileges the helper's\n * `/proc/<pid>/environ` becomes readable by every other sandbox process.\n */\nfunction runAsSandboxUser(command: [string, ...string[]]): child_process.SpawnSyncReturns<Buffer> {\n return child_process.spawnSync(SUDO_PATH, ['-u', sandboxUserName as string, ...command], { env: MINIMAL_ENV });\n}\n\n/**\n * `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose\n * permissions block deletion: let the sandbox user reopen its own files first, then retry. The\n * containing directory is relaxed as well — unlinking an entry needs write permission on its\n * parent, so a submission that chmods a directory it owns would otherwise make everything inside it\n * undeletable.\n */\nexport async function forceRemove(targetPath: string): Promise<void> {\n try {\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n } catch (error) {\n if (!sandboxUserName) throw error;\n relaxPermissionsAsSandboxUser(targetPath);\n // Not recursive: only the containing directory's own write bit governs the unlink.\n runAsSandboxUser(['chmod', 'a+rwX', path.dirname(targetPath)]);\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs");let n=require("node:child_process");n=e.__toESM(n,1);let r=require("node:os");r=e.__toESM(r,1);const i=[r.default.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`];function a(e,r,a,o){let s=Date.now(),c={...a.env??process.env},l=t.wrapCommandWithSandboxUser([t.TIMEOUT_COMMAND,o.toFixed(3),...i,e,...r]),u=t.startSandboxTimeoutWatchdog(o),d,f;try{d=n.default.spawnSync(l[0],l.slice(1),{...a,env:{...c,...t.getSandboxUserEnvOverrides(c)}})}finally{f=u.fired(),u.cancel(),t.sandboxUserName&&t.killSandboxUserProcesses()}let p=Date.now(),m=/(?:^|\n)(\d+\.\d+) (\d+)\s*$/.exec(d.stderr),h=m?d.stderr.slice(0,m.index):d.stderr,g=Number(m?.[1])||(p-s)/1e3,_=Number(m?.[2])*1024||0;return d.status===124||f?{...d,status:0,stderr:h,timeSeconds:o+.001,memoryBytes:_}:{...d,stderr:h,timeSeconds:g,memoryBytes:_}}exports.spawnSyncWithTimeout=a;
|
|
2
2
|
//# sourceMappingURL=spawnSyncWithTimeout.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawnSyncWithTimeout.cjs","names":["os","child_process"],"sources":["../../src/helpers/spawnSyncWithTimeout.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport os from 'node:os';\n\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nexport function spawnSyncWithTimeout(\n command: string,\n args: readonly string[],\n options: child_process.SpawnSyncOptionsWithStringEncoding,\n timeoutSeconds: number\n): child_process.SpawnSyncReturns<string> & { timeSeconds: number; memoryBytes: number } {\n const startTimeMilliseconds = Date.now();\n\n const
|
|
1
|
+
{"version":3,"file":"spawnSyncWithTimeout.cjs","names":["os","wrapCommandWithSandboxUser","TIMEOUT_COMMAND","startSandboxTimeoutWatchdog","child_process","getSandboxUserEnvOverrides","sandboxUserName"],"sources":["../../src/helpers/spawnSyncWithTimeout.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport os from 'node:os';\n\nimport {\n getSandboxUserEnvOverrides,\n killSandboxUserProcesses,\n sandboxUserName,\n startSandboxTimeoutWatchdog,\n TIMEOUT_COMMAND,\n wrapCommandWithSandboxUser,\n} from './sandboxUser.js';\n\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\n/**\n * Run an untrusted (submission-derived) command with a timeout. When a judge server delegates\n * untrusted execution via `EXERCODE_SANDBOX_USER` (see `sandboxUser.ts`), the whole command,\n * including `timeout`, runs as the sandbox user so the timer can signal the sandboxed process; a\n * harness-owned watchdog enforces the deadline even if the submission kills its own `timeout`, and\n * leftover sandbox processes (e.g. daemonized children) are killed after every run.\n */\nexport function spawnSyncWithTimeout(\n command: string,\n args: readonly string[],\n options: child_process.SpawnSyncOptionsWithStringEncoding,\n timeoutSeconds: number\n): child_process.SpawnSyncReturns<string> & { timeSeconds: number; memoryBytes: number } {\n const startTimeMilliseconds = Date.now();\n\n const env = { ...(options.env ?? process.env) };\n const wrappedCommand = wrapCommandWithSandboxUser([\n TIMEOUT_COMMAND,\n timeoutSeconds.toFixed(3),\n ...TIME_COMMAND,\n command,\n ...args,\n ]);\n const watchdog = startSandboxTimeoutWatchdog(timeoutSeconds);\n let spawnResult: child_process.SpawnSyncReturns<string>;\n let watchdogFired: boolean;\n try {\n spawnResult = child_process.spawnSync(wrappedCommand[0], wrappedCommand.slice(1), {\n ...options,\n env: { ...env, ...getSandboxUserEnvOverrides(env) },\n });\n } finally {\n // Read the watchdog state before cancelling, then always cancel: a leaked watchdog is detached\n // and would SIGKILL a later request's submission.\n watchdogFired = watchdog.fired();\n watchdog.cancel();\n if (sandboxUserName) killSandboxUserProcesses();\n }\n\n const stopTimeMilliseconds = Date.now();\n\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(spawnResult.stderr);\n const stderr = match ? spawnResult.stderr.slice(0, match.index) : spawnResult.stderr;\n const timeSeconds = Number(match?.[1]) || (stopTimeMilliseconds - startTimeMilliseconds) / 1000;\n const memoryBytes = Number(match?.[2]) * 1024 || 0;\n\n // `timeout` reports 124, but a sandboxed submission can kill its own same-UID `timeout` and be\n // stopped by the watchdog instead, which surfaces as a signal-derived status. Both are timeouts:\n // reporting the latter as a runtime error would mislabel a submission that outran its deadline.\n if (spawnResult.status === 124 || watchdogFired) {\n return { ...spawnResult, status: 0, stderr, timeSeconds: timeoutSeconds + 1e-3, memoryBytes };\n }\n\n return { ...spawnResult, stderr, timeSeconds, memoryBytes };\n}\n"],"mappings":"gMAYA,MAAM,EAAe,CAACA,EAAAA,QAAG,SAAS,IAAM,SAAW,QAAU,gBAAiB,WAAY,OAAO,EASjG,SAAgB,EACd,EACA,EACA,EACA,EACuF,CACvF,IAAM,EAAwB,KAAK,IAAI,EAEjC,EAAM,CAAE,GAAI,EAAQ,KAAO,QAAQ,GAAK,EACxC,EAAiBC,EAAAA,2BAA2B,CAChDC,EAAAA,gBACA,EAAe,QAAQ,CAAC,EACxB,GAAG,EACH,EACA,GAAG,CACL,CAAC,EACK,EAAWC,EAAAA,4BAA4B,CAAc,EACvD,EACA,EACJ,GAAI,CACF,EAAcC,EAAAA,QAAc,UAAU,EAAe,GAAI,EAAe,MAAM,CAAC,EAAG,CAChF,GAAG,EACH,IAAK,CAAE,GAAG,EAAK,GAAGC,EAAAA,2BAA2B,CAAG,CAAE,CACpD,CAAC,CACH,QAAU,CAGR,EAAgB,EAAS,MAAM,EAC/B,EAAS,OAAO,EACZC,EAAAA,iBAAiB,EAAA,yBAAyB,CAChD,CAEA,IAAM,EAAuB,KAAK,IAAI,EAEhC,EAAQ,+BAA+B,KAAK,EAAY,MAAM,EAC9D,EAAS,EAAQ,EAAY,OAAO,MAAM,EAAG,EAAM,KAAK,EAAI,EAAY,OACxE,EAAc,OAAO,IAAQ,EAAE,IAAM,EAAuB,GAAyB,IACrF,EAAc,OAAO,IAAQ,EAAE,EAAI,MAAQ,EASjD,OAJI,EAAY,SAAW,KAAO,EACzB,CAAE,GAAG,EAAa,OAAQ,EAAG,SAAQ,YAAa,EAAiB,KAAM,aAAY,EAGvF,CAAE,GAAG,EAAa,SAAQ,cAAa,aAAY,CAC5D"}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import child_process from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Run an untrusted (submission-derived) command with a timeout. When a judge server delegates
|
|
4
|
+
* untrusted execution via `EXERCODE_SANDBOX_USER` (see `sandboxUser.ts`), the whole command,
|
|
5
|
+
* including `timeout`, runs as the sandbox user so the timer can signal the sandboxed process; a
|
|
6
|
+
* harness-owned watchdog enforces the deadline even if the submission kills its own `timeout`, and
|
|
7
|
+
* leftover sandbox processes (e.g. daemonized children) are killed after every run.
|
|
8
|
+
*/
|
|
2
9
|
export declare function spawnSyncWithTimeout(command: string, args: readonly string[], options: child_process.SpawnSyncOptionsWithStringEncoding, timeoutSeconds: number): child_process.SpawnSyncReturns<string> & {
|
|
3
10
|
timeSeconds: number;
|
|
4
11
|
memoryBytes: number;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"node:child_process";import
|
|
1
|
+
import{TIMEOUT_COMMAND as e,getSandboxUserEnvOverrides as t,killSandboxUserProcesses as n,sandboxUserName as r,startSandboxTimeoutWatchdog as i,wrapCommandWithSandboxUser as a}from"./sandboxUser.js";import o from"node:child_process";import s from"node:os";const c=[s.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`];function l(s,l,u,d){let f=Date.now(),p={...u.env??process.env},m=a([e,d.toFixed(3),...c,s,...l]),h=i(d),g,_;try{g=o.spawnSync(m[0],m.slice(1),{...u,env:{...p,...t(p)}})}finally{_=h.fired(),h.cancel(),r&&n()}let v=Date.now(),y=/(?:^|\n)(\d+\.\d+) (\d+)\s*$/.exec(g.stderr),b=y?g.stderr.slice(0,y.index):g.stderr,x=Number(y?.[1])||(v-f)/1e3,S=Number(y?.[2])*1024||0;return g.status===124||_?{...g,status:0,stderr:b,timeSeconds:d+.001,memoryBytes:S}:{...g,stderr:b,timeSeconds:x,memoryBytes:S}}export{l as spawnSyncWithTimeout};
|
|
2
2
|
//# sourceMappingURL=spawnSyncWithTimeout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawnSyncWithTimeout.js","names":[
|
|
1
|
+
{"version":3,"file":"spawnSyncWithTimeout.js","names":[],"sources":["../../src/helpers/spawnSyncWithTimeout.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport os from 'node:os';\n\nimport {\n getSandboxUserEnvOverrides,\n killSandboxUserProcesses,\n sandboxUserName,\n startSandboxTimeoutWatchdog,\n TIMEOUT_COMMAND,\n wrapCommandWithSandboxUser,\n} from './sandboxUser.js';\n\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\n/**\n * Run an untrusted (submission-derived) command with a timeout. When a judge server delegates\n * untrusted execution via `EXERCODE_SANDBOX_USER` (see `sandboxUser.ts`), the whole command,\n * including `timeout`, runs as the sandbox user so the timer can signal the sandboxed process; a\n * harness-owned watchdog enforces the deadline even if the submission kills its own `timeout`, and\n * leftover sandbox processes (e.g. daemonized children) are killed after every run.\n */\nexport function spawnSyncWithTimeout(\n command: string,\n args: readonly string[],\n options: child_process.SpawnSyncOptionsWithStringEncoding,\n timeoutSeconds: number\n): child_process.SpawnSyncReturns<string> & { timeSeconds: number; memoryBytes: number } {\n const startTimeMilliseconds = Date.now();\n\n const env = { ...(options.env ?? process.env) };\n const wrappedCommand = wrapCommandWithSandboxUser([\n TIMEOUT_COMMAND,\n timeoutSeconds.toFixed(3),\n ...TIME_COMMAND,\n command,\n ...args,\n ]);\n const watchdog = startSandboxTimeoutWatchdog(timeoutSeconds);\n let spawnResult: child_process.SpawnSyncReturns<string>;\n let watchdogFired: boolean;\n try {\n spawnResult = child_process.spawnSync(wrappedCommand[0], wrappedCommand.slice(1), {\n ...options,\n env: { ...env, ...getSandboxUserEnvOverrides(env) },\n });\n } finally {\n // Read the watchdog state before cancelling, then always cancel: a leaked watchdog is detached\n // and would SIGKILL a later request's submission.\n watchdogFired = watchdog.fired();\n watchdog.cancel();\n if (sandboxUserName) killSandboxUserProcesses();\n }\n\n const stopTimeMilliseconds = Date.now();\n\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(spawnResult.stderr);\n const stderr = match ? spawnResult.stderr.slice(0, match.index) : spawnResult.stderr;\n const timeSeconds = Number(match?.[1]) || (stopTimeMilliseconds - startTimeMilliseconds) / 1000;\n const memoryBytes = Number(match?.[2]) * 1024 || 0;\n\n // `timeout` reports 124, but a sandboxed submission can kill its own same-UID `timeout` and be\n // stopped by the watchdog instead, which surfaces as a signal-derived status. Both are timeouts:\n // reporting the latter as a runtime error would mislabel a submission that outran its deadline.\n if (spawnResult.status === 124 || watchdogFired) {\n return { ...spawnResult, status: 0, stderr, timeSeconds: timeoutSeconds + 1e-3, memoryBytes };\n }\n\n return { ...spawnResult, stderr, timeSeconds, memoryBytes };\n}\n"],"mappings":"gQAYA,MAAM,EAAe,CAAC,EAAG,SAAS,IAAM,SAAW,QAAU,gBAAiB,WAAY,OAAO,EASjG,SAAgB,EACd,EACA,EACA,EACA,EACuF,CACvF,IAAM,EAAwB,KAAK,IAAI,EAEjC,EAAM,CAAE,GAAI,EAAQ,KAAO,QAAQ,GAAK,EACxC,EAAiB,EAA2B,CAChD,EACA,EAAe,QAAQ,CAAC,EACxB,GAAG,EACH,EACA,GAAG,CACL,CAAC,EACK,EAAW,EAA4B,CAAc,EACvD,EACA,EACJ,GAAI,CACF,EAAc,EAAc,UAAU,EAAe,GAAI,EAAe,MAAM,CAAC,EAAG,CAChF,GAAG,EACH,IAAK,CAAE,GAAG,EAAK,GAAG,EAA2B,CAAG,CAAE,CACpD,CAAC,CACH,QAAU,CAGR,EAAgB,EAAS,MAAM,EAC/B,EAAS,OAAO,EACZ,GAAiB,EAAyB,CAChD,CAEA,IAAM,EAAuB,KAAK,IAAI,EAEhC,EAAQ,+BAA+B,KAAK,EAAY,MAAM,EAC9D,EAAS,EAAQ,EAAY,OAAO,MAAM,EAAG,EAAM,KAAK,EAAI,EAAY,OACxE,EAAc,OAAO,IAAQ,EAAE,IAAM,EAAuB,GAAyB,IACrF,EAAc,OAAO,IAAQ,EAAE,EAAI,MAAQ,EASjD,OAJI,EAAY,SAAW,KAAO,EACzB,CAAE,GAAG,EAAa,OAAQ,EAAG,SAAQ,YAAa,EAAiB,KAAM,aAAY,EAGvF,CAAE,GAAG,EAAa,SAAQ,cAAa,aAAY,CAC5D"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs"),n=require("./safeFs.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:http");a=e.__toESM(a,1);function o(e){let t=a.default.createServer((t,n)=>{let a=new URL(t.url??`/`,`http://127.0.0.1`).pathname;a===`/`&&(a=`/index.html`);let o=a.endsWith(`/`)?i.default.join(a,`index.html`):a,s=i.default.join(e,o);if(!c(e,s)){n.statusCode=404,n.end();return}if(r.default.existsSync(s))try{let e=r.default.readFileSync(s);n.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":l(o)}),n.end(e)}catch{n.statusCode=500,n.end()}else n.statusCode=404,n.end()});t.listen();let n=t.address();if(!n)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{t.closeAllConnections(),await new Promise(e=>{t.close(()=>{e()})})},url:typeof n==`string`?n:`http://127.0.0.1:${n.port}`,port:typeof n==`object`?n.port:void 0}}const s={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function c(e,i){if(!t.sandboxUserName)return!0;try{return n.isContainedPath(r.default.realpathSync(e),r.default.realpathSync(i))}catch{return!r.default.existsSync(i)}}function l(e){for(let[t,n]of Object.entries(s))if(e.endsWith(t))return n;return`text/plain`}exports.startHttpServer=o;
|
|
2
2
|
//# sourceMappingURL=startHttpServer.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startHttpServer.cjs","names":["http","path","fs"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"startHttpServer.cjs","names":["http","path","fs","sandboxUserName","isContainedPath"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nimport { isContainedPath } from './safeFs.js';\nimport { sandboxUserName } from './sandboxUser.js';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n // Under delegation this read runs as the trusted harness user while the served directory\n // belongs to the sandboxed submission, so a planted symlink (or symlinked parent) pointing at\n // e.g. the problem's expected outputs would otherwise be served straight back to the page.\n if (!isServableSubmissionPath(dir, filePath)) {\n response.statusCode = 404;\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction isServableSubmissionPath(dir: string, filePath: string): boolean {\n // Without user separation the page's own code can read these files anyway, so keep the previous\n // behavior and stay usable for course authoring outside the judge image.\n if (!sandboxUserName) return true;\n try {\n return isContainedPath(fs.realpathSync(dir), fs.realpathSync(filePath));\n } catch {\n // A missing file is reported as 404 below; anything unresolvable is not servable either.\n return !fs.existsSync(filePath);\n }\n}\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"6PAgBA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAASA,EAAAA,QAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EAAW,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SAE3D,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAIC,EAAAA,QAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAWA,EAAAA,QAAK,KAAK,EAAK,CAAqB,EAKrD,GAAI,CAAC,EAAyB,EAAK,CAAQ,EAAG,CAC5C,EAAS,WAAa,IACtB,EAAS,IAAI,EACb,MACF,CAEA,GAAIC,EAAAA,QAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAASA,EAAAA,QAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAyB,EAAa,EAA2B,CAGxE,GAAI,CAACC,EAAAA,gBAAiB,MAAO,GAC7B,GAAI,CACF,OAAOC,EAAAA,gBAAgBF,EAAAA,QAAG,aAAa,CAAG,EAAGA,EAAAA,QAAG,aAAa,CAAQ,CAAC,CACxE,MAAQ,CAEN,MAAO,CAACA,EAAAA,QAAG,WAAW,CAAQ,CAChC,CACF,CAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"node:
|
|
1
|
+
import{sandboxUserName as e}from"./sandboxUser.js";import{isContainedPath as t}from"./safeFs.js";import n from"node:fs";import r from"node:path";import i from"node:http";function a(e){let t=i.createServer((t,i)=>{let a=new URL(t.url??`/`,`http://127.0.0.1`).pathname;a===`/`&&(a=`/index.html`);let o=a.endsWith(`/`)?r.join(a,`index.html`):a,l=r.join(e,o);if(!s(e,l)){i.statusCode=404,i.end();return}if(n.existsSync(l))try{let e=n.readFileSync(l);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":c(o)}),i.end(e)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});t.listen();let a=t.address();if(!a)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{t.closeAllConnections(),await new Promise(e=>{t.close(()=>{e()})})},url:typeof a==`string`?a:`http://127.0.0.1:${a.port}`,port:typeof a==`object`?a.port:void 0}}const o={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function s(r,i){if(!e)return!0;try{return t(n.realpathSync(r),n.realpathSync(i))}catch{return!n.existsSync(i)}}function c(e){for(let[t,n]of Object.entries(o))if(e.endsWith(t))return n;return`text/plain`}export{a as startHttpServer};
|
|
2
2
|
//# sourceMappingURL=startHttpServer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startHttpServer.js","names":[],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"startHttpServer.js","names":[],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nimport { isContainedPath } from './safeFs.js';\nimport { sandboxUserName } from './sandboxUser.js';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n // Under delegation this read runs as the trusted harness user while the served directory\n // belongs to the sandboxed submission, so a planted symlink (or symlinked parent) pointing at\n // e.g. the problem's expected outputs would otherwise be served straight back to the page.\n if (!isServableSubmissionPath(dir, filePath)) {\n response.statusCode = 404;\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction isServableSubmissionPath(dir: string, filePath: string): boolean {\n // Without user separation the page's own code can read these files anyway, so keep the previous\n // behavior and stay usable for course authoring outside the judge image.\n if (!sandboxUserName) return true;\n try {\n return isContainedPath(fs.realpathSync(dir), fs.realpathSync(filePath));\n } catch {\n // A missing file is reported as 404 below; anything unresolvable is not servable either.\n return !fs.existsSync(filePath);\n }\n}\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"0KAgBA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EAAW,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SAE3D,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAI,EAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAW,EAAK,KAAK,EAAK,CAAqB,EAKrD,GAAI,CAAC,EAAyB,EAAK,CAAQ,EAAG,CAC5C,EAAS,WAAa,IACtB,EAAS,IAAI,EACb,MACF,CAEA,GAAI,EAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAAS,EAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAyB,EAAa,EAA2B,CAGxE,GAAI,CAAC,EAAiB,MAAO,GAC7B,GAAI,CACF,OAAO,EAAgB,EAAG,aAAa,CAAG,EAAG,EAAG,aAAa,CAAQ,CAAC,CACxE,MAAQ,CAEN,MAAO,CAAC,EAAG,WAAW,CAAQ,CAChC,CACF,CAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/parseArgs.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/removeCommentsInSourceCode.cjs"),i=require("./helpers/
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/parseArgs.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/removeCommentsInSourceCode.cjs"),i=require("./helpers/sandboxUser.cjs"),a=require("./helpers/safeFs.cjs"),o=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),s=require("./helpers/runPythonInTemporaryUvProject.cjs"),c=require("./helpers/sourceCodeGrammars.cjs"),l=require("./helpers/startHttpServer.cjs"),u=require("./types/decisionCode.cjs");exports.DecisionCode=u.DecisionCode,exports.SANDBOX_ENV_PREFIX=i.SANDBOX_ENV_PREFIX,exports.SANDBOX_USER_ENV_NAME=i.SANDBOX_USER_ENV_NAME,exports.SANDBOX_WATCHDOG_GRACE_SECONDS=i.SANDBOX_WATCHDOG_GRACE_SECONDS,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.TIMEOUT_COMMAND=i.TIMEOUT_COMMAND,exports.copyPackageManagerProjectFiles=o.copyPackageManagerProjectFiles,exports.copyWithoutFollowingSymlinks=a.copyWithoutFollowingSymlinks,exports.createDirectoryWithoutFollowingSymlinks=a.createDirectoryWithoutFollowingSymlinks,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.forceRemove=i.forceRemove,exports.getSandboxUserEnvOverrides=i.getSandboxUserEnvOverrides,exports.getTrustedHelperEnv=i.getTrustedHelperEnv,exports.isContainedPath=a.isContainedPath,exports.isSandboxWrappedCommand=i.isSandboxWrappedCommand,exports.killSandboxUserProcesses=i.killSandboxUserProcesses,exports.languageIdToSourceCodeGrammar=c.languageIdToSourceCodeGrammar,exports.makeAccessibleToSandboxUser=i.makeAccessibleToSandboxUser,exports.parseArgs=e.parseArgs,exports.prependInsideSandboxWrapper=i.prependInsideSandboxWrapper,exports.printTestCaseResult=n.printTestCaseResult,exports.relaxPermissionsAsSandboxUser=i.relaxPermissionsAsSandboxUser,exports.removeCommentsAndStringsInSourceCode=r.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=r.removeCommentsInSourceCode,exports.runCommandInTemporaryPackageManagerProject=o.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=s.runPythonInTemporaryUvProject,exports.sandboxUserName=i.sandboxUserName,exports.startHttpServer=l.startHttpServer,exports.startSandboxTimeoutWatchdog=i.startSandboxTimeoutWatchdog,exports.testCaseResultSchema=t.testCaseResultSchema,exports.wrapCommandWithSandboxUser=i.wrapCommandWithSandboxUser,exports.writeFileWithoutFollowingSymlinks=a.writeFileWithoutFollowingSymlinks;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from './helpers/printTestCaseResult.js';
|
|
|
3
3
|
export * from './helpers/removeCommentsInSourceCode.js';
|
|
4
4
|
export * from './helpers/runCommandInTemporaryPackageManagerProject.js';
|
|
5
5
|
export * from './helpers/runPythonInTemporaryUvProject.js';
|
|
6
|
+
export * from './helpers/safeFs.js';
|
|
7
|
+
export * from './helpers/sandboxUser.js';
|
|
6
8
|
export * from './helpers/sourceCodeGrammars.js';
|
|
7
9
|
export * from './helpers/startHttpServer.js';
|
|
8
10
|
export * from './types/decisionCode.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseArgs as e}from"./helpers/parseArgs.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{removeCommentsAndStringsInSourceCode as a,removeCommentsInSourceCode as o}from"./helpers/removeCommentsInSourceCode.js";import{
|
|
1
|
+
import{parseArgs as e}from"./helpers/parseArgs.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{removeCommentsAndStringsInSourceCode as a,removeCommentsInSourceCode as o}from"./helpers/removeCommentsInSourceCode.js";import{SANDBOX_ENV_PREFIX as s,SANDBOX_USER_ENV_NAME as c,SANDBOX_WATCHDOG_GRACE_SECONDS as l,TIMEOUT_COMMAND as u,forceRemove as d,getSandboxUserEnvOverrides as f,getTrustedHelperEnv as p,isSandboxWrappedCommand as m,killSandboxUserProcesses as h,makeAccessibleToSandboxUser as g,prependInsideSandboxWrapper as _,relaxPermissionsAsSandboxUser as v,sandboxUserName as y,startSandboxTimeoutWatchdog as b,wrapCommandWithSandboxUser as x}from"./helpers/sandboxUser.js";import{copyWithoutFollowingSymlinks as S,createDirectoryWithoutFollowingSymlinks as C,isContainedPath as w,writeFileWithoutFollowingSymlinks as T}from"./helpers/safeFs.js";import{copyPackageManagerProjectFiles as E,runCommandInTemporaryPackageManagerProject as D}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as O}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as k}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as A}from"./helpers/startHttpServer.js";import{DecisionCode as j}from"./types/decisionCode.js";export{j as DecisionCode,s as SANDBOX_ENV_PREFIX,c as SANDBOX_USER_ENV_NAME,l as SANDBOX_WATCHDOG_GRACE_SECONDS,t as TEST_CASE_RESULT_PREFIX,u as TIMEOUT_COMMAND,E as copyPackageManagerProjectFiles,S as copyWithoutFollowingSymlinks,C as createDirectoryWithoutFollowingSymlinks,r as encodeFileForTestCaseResult,d as forceRemove,f as getSandboxUserEnvOverrides,p as getTrustedHelperEnv,w as isContainedPath,m as isSandboxWrappedCommand,h as killSandboxUserProcesses,k as languageIdToSourceCodeGrammar,g as makeAccessibleToSandboxUser,e as parseArgs,_ as prependInsideSandboxWrapper,i as printTestCaseResult,v as relaxPermissionsAsSandboxUser,a as removeCommentsAndStringsInSourceCode,o as removeCommentsInSourceCode,D as runCommandInTemporaryPackageManagerProject,O as runPythonInTemporaryUvProject,y as sandboxUserName,A as startHttpServer,b as startSandboxTimeoutWatchdog,n as testCaseResultSchema,x as wrapCommandWithSandboxUser,T as writeFileWithoutFollowingSymlinks};
|
package/dist/presets/command.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/parseArgs.cjs"),n=require("../helpers/printTestCaseResult.cjs"),r=require("../types/decisionCode.cjs"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/parseArgs.cjs"),n=require("../helpers/printTestCaseResult.cjs"),r=require("../helpers/sandboxUser.cjs"),i=require("../types/decisionCode.cjs"),a=require("../helpers/printDebugBanner.cjs"),o=require("../helpers/checkProblemDirIsolation.cjs"),s=require("../helpers/cleanWorkingDirectory.cjs"),c=require("../helpers/copyTestCaseFileInput.cjs"),l=require("../helpers/findEntryPointFile.cjs"),u=require("../helpers/findLanguageDefinitionByPath.cjs"),d=require("../helpers/judgeByStaticAnalysis.cjs"),f=require("../helpers/readOutputFiles.cjs"),p=require("../helpers/runCustomRunner.cjs"),m=require("../helpers/readProblemMarkdownFrontMatter.cjs"),h=require("../helpers/readTestCases.cjs"),g=require("../helpers/resolveCwds.cjs"),_=require("../helpers/spawnSyncWithTimeout.cjs");let v=require("zod"),y=require("node:path");y=e.__toESM(y,1);const b=v.z.object({language:v.z.union([v.z.string(),v.z.array(v.z.string())]).optional()});async function x(e,n){let r=t.parseArgs(process.argv),i=b.parse(r.params),{cwds:s,isDebugMode:c}=await g.resolveCwds(e,r.cwd);if(c){let t=s.find(e=>e.expectedResult===`accepted`);if(t){if(!(await o.checkProblemDirIsolation(e,t,i)).passed){process.exitCode=1;return}}else a.printDebugBanner([`[DEBUG MODE] isolated problem directory check skipped`,``,`No accepted model answer is available for checking that the copied judge still accepts a valid submission.`])}for(let t of s){c&&g.printDebugCwdBanner(e,t);let r=await S(e,t.cwd,i,n);c&&!C(t,r)&&(process.exitCode=1,g.printDebugExpectationFailureBanner(e,t))}}async function S(e,t,a,o){r.makeAccessibleToSandboxUser(t);let h=await m.readProblemMarkdownFrontMatter(e),g=await(o.readTestCases??D)(e),_=g[0]?.id??`prebuild`,v={buildTimeoutSeconds:o.limits?.buildTimeoutSeconds??10,maxOutputLength:o.limits?.maxOutputLength??5e4},y=o.runTimeoutSeconds??2,b=await d.judgeByStaticAnalysis(t,h);if(b)return n.printTestCaseResult({testCaseId:_,...b}),{allAccepted:!1};let x=await l.findEntryPointFile(t,a.language);if(!x)return n.printTestCaseResult({testCaseId:_,decisionCode:i.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:`main file not found${a.language?`: language: ${a.language}`:``}`}),{allAccepted:!1};let S=u.findLanguageDefinitionByPath(x);if(!S)return n.printTestCaseResult({testCaseId:_,decisionCode:i.DecisionCode.WRONG_ANSWER,stderr:`unsupported language`}),{allAccepted:!1};let C={...process.env,CI:``,FORCE_COLOR:`0`},E=x;if(S.prebuild)try{await S.prebuild(t);let e=await l.findEntryPointFile(t,a.language);e&&(E=e)}catch(e){return n.printTestCaseResult({testCaseId:_,decisionCode:i.DecisionCode.BUILD_ERROR,stderr:e instanceof Error?e.message:String(e)}),{allAccepted:!1}}let A=S.buildCommand?.(E);if(A){let e=w(A,{cwd:t,env:C,testCaseId:_,limits:v});if(e)return n.printTestCaseResult(e),{allAccepted:!1}}let j=await s.snapshotWorkingDirectory(t);if(g.length===0)return n.printTestCaseResult({testCaseId:`default`,decisionCode:i.DecisionCode.ACCEPTED}),{allAccepted:!0};let M=g.shared?.fileInputPath;for(let e of g){M&&await c.copyTestCaseFileInput(M,t),e.fileInputPath&&await c.copyTestCaseFileInput(e.fileInputPath,t);let a=typeof h.timeLimitMs==`number`?h.timeLimitMs/1e3:y,l=S.command(E),u=e.input??``,d;try{o.resolveInput&&(u=await o.resolveInput({testCase:e,cwd:t,env:C})),d=o.runCommand?await p.runCustomRunner(()=>o.runCommand?.({testCase:e,command:r.wrapCommandWithSandboxUser(l),stdin:u,cwd:t,env:{...C,...r.getSandboxUserEnvOverrides(C)},timeLimitSeconds:a})):O(l,{stdin:u,cwd:t,env:C,timeLimitSeconds:a})}catch(r){return n.printTestCaseResult({testCaseId:e.id,decisionCode:i.DecisionCode.RUNTIME_ERROR,stdin:u,stderr:T(r)}),await s.cleanWorkingDirectory(t,j),{allAccepted:!1}}let m=await f.readOutputFiles(t,h.requiredOutputFilePaths??[]),g={timeLimitSeconds:a,outputLimitLength:v.maxOutputLength,problemMarkdownFrontMatter:{memoryLimitByte:h.memoryLimitByte,requiredOutputFilePaths:h.requiredOutputFilePaths}},_=k({runResult:d,outputFiles:m,context:g}),b=_;if(_.decisionCode===i.DecisionCode.ACCEPTED)try{let t=await o.test?.({testCase:e,runResult:d,outputFiles:m,context:g});t&&(b={decisionCode:t.decisionCode??_.decisionCode,feedbackMarkdown:t.feedbackMarkdown,stderr:t.stderr})}catch(e){b={decisionCode:i.DecisionCode.RUNTIME_ERROR,stderr:T(e)}}if(n.printTestCaseResult({testCaseId:e.id,...b,exitStatus:d.status,stdin:d.stdin,stdout:d.stdout.slice(0,v.maxOutputLength)||void 0,stderr:(b.stderr??d.stderr).slice(0,v.maxOutputLength)||void 0,timeSeconds:d.timeSeconds,memoryBytes:d.memoryBytes,outputFiles:m.length>0?m:void 0}),await s.cleanWorkingDirectory(t,j),b.decisionCode!==i.DecisionCode.ACCEPTED)return{allAccepted:!1}}return{allAccepted:!0}}function C(e,t){return t.allAccepted===(e.expectedResult===`accepted`)}function w(e,t){let n=_.spawnSyncWithTimeout(e[0],e.slice(1),{cwd:t.cwd,encoding:`utf8`,env:t.env},t.limits.buildTimeoutSeconds),r=n.status??void 0;if(n.status!==0)return{testCaseId:t.testCaseId,decisionCode:i.DecisionCode.BUILD_ERROR,exitStatus:r,stdout:n.stdout.slice(0,t.limits.maxOutputLength),stderr:n.stderr.slice(0,t.limits.maxOutputLength),timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes};if(n.timeSeconds>t.limits.buildTimeoutSeconds)return{testCaseId:t.testCaseId,decisionCode:i.DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,exitStatus:r,stdout:n.stdout.slice(0,t.limits.maxOutputLength),stderr:n.stderr.slice(0,t.limits.maxOutputLength),timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes};if(n.stdout.length>t.limits.maxOutputLength||n.stderr.length>t.limits.maxOutputLength)return{testCaseId:t.testCaseId,decisionCode:i.DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,exitStatus:r,stdout:n.stdout.slice(0,t.limits.maxOutputLength),stderr:n.stderr.slice(0,t.limits.maxOutputLength),timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes}}function T(e){return e instanceof Error?e.message:String(e)}function E(e){return{id:e.id,input:e.input,fileInputPath:e.fileInputPath}}async function D(e){let t=await h.readTestCases(y.default.join(e,`test_cases`)),n=t.map(e=>E(e));return t.shared?.fileInputPath?Object.assign(n,{shared:{fileInputPath:t.shared.fileInputPath}}):n}function O(e,t){let n=_.spawnSyncWithTimeout(e[0],e.slice(1),{cwd:t.cwd,encoding:`utf8`,input:t.stdin,env:t.env},t.timeLimitSeconds);return{stdin:t.stdin,stdout:n.stdout,stderr:n.stderr,status:n.status??void 0,timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes}}function k(e){if(e.runResult.status!==0)return{decisionCode:i.DecisionCode.RUNTIME_ERROR,stderr:e.runResult.stderr};if(e.runResult.timeSeconds>e.context.timeLimitSeconds)return{decisionCode:i.DecisionCode.TIME_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.memoryBytes>(e.context.problemMarkdownFrontMatter.memoryLimitByte??1/0))return{decisionCode:i.DecisionCode.MEMORY_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.outputLimitExceeded||e.runResult.stdout.length>e.context.outputLimitLength||e.runResult.stderr.length>e.context.outputLimitLength)return{decisionCode:i.DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:e.runResult.stderr};let t=e.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length??0;return e.outputFiles.length<t?{decisionCode:i.DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR}:{decisionCode:i.DecisionCode.ACCEPTED}}exports.commandJudgePreset=x;
|
|
2
2
|
//# sourceMappingURL=command.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.cjs","names":["z","parseArgs","resolveCwds","checkProblemDirIsolation","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","spawnSyncWithTimeout","readFileTestCases","path"],"sources":["../../src/presets/command.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { z } from 'zod';\n\nimport { checkProblemDirIsolation } from '../helpers/checkProblemDirIsolation.js';\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 { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printDebugBanner } from '../helpers/printDebugBanner.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 {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { spawnSyncWithTimeout } from '../helpers/spawnSyncWithTimeout.js';\nimport { DecisionCode } from '../types/decisionCode.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 = 2;\nconst MAX_STDOUT_LENGTH = 50_000;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\ntype JudgeParams = z.infer<typeof judgeParamsSchema>;\n\ninterface BaseCommandTestCase {\n id: string;\n input?: string;\n fileInputPath?: string;\n}\n\ntype CommandJudgeCaseResult = Pick<TestCaseResult, 'decisionCode' | 'feedbackMarkdown' | 'stderr'>;\n\nexport interface CommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded?: boolean;\n}\n\ninterface CommandJudgeContext {\n timeLimitSeconds: number;\n outputLimitLength: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\nexport interface CommandJudgeLimits {\n buildTimeoutSeconds: number;\n maxOutputLength: number;\n}\n\nexport interface CommandJudgePresetOptions<\n TTestCase extends BaseCommandTestCase = BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n> {\n limits?: CommandJudgeLimits;\n runTimeoutSeconds?: number;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<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 }) => Promise<TRunResult> | TRunResult;\n test?: (context: {\n testCase: TTestCase;\n runResult: TRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: CommandJudgeContext;\n }) => Promise<Partial<CommandJudgeCaseResult>> | Partial<CommandJudgeCaseResult> | undefined;\n}\n\n/**\n * A preset function for judging by executable command.\n *\n * Keep problem-specific logic in `resolveInput` and `test`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { commandJudgePreset } from '@exercode/problem-utils/presets/command';\n * import { DecisionCode } from '@exercode/problem-utils';\n *\n * await commandJudgePreset(import.meta.dirname, {\n * readTestCases: async () => [\n * { id: '01', input: '1 2' },\n * ],\n * test: ({ runResult }) => {\n * return runResult.stdout.trim() === '3'\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n *\n * Run with the required parameters:\n * ```bash\n * bun judge.ts model_answers/python '{ \"language\": \"python\" }'\n * ```\n *\n * Run without a cwd argument to judge each `<problemDir>/model_answers/*` directory\n * for debugging. A prominent banner is printed for each run.\n * ```bash\n * bun judge.ts\n * ```\n */\nexport async function commandJudgePreset<\n TTestCase extends BaseCommandTestCase = BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n>(problemDir: string, options: CommandJudgePresetOptions<TTestCase, TRunResult>): Promise<void> {\n const args = parseArgs(process.argv);\n const params = judgeParamsSchema.parse(args.params);\n\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode) {\n const acceptedCwd = cwds.find((cwd) => cwd.expectedResult === 'accepted');\n if (acceptedCwd) {\n const isolationCheckResult = await checkProblemDirIsolation(problemDir, acceptedCwd, params);\n if (!isolationCheckResult.passed) {\n process.exitCode = 1;\n return;\n }\n } else {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'No accepted model answer is available for checking that the copied judge still accepts a valid submission.',\n ]);\n }\n }\n\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await runCommandJudgeForCwd<TTestCase, TRunResult>(problemDir, resolvedCwd.cwd, params, options);\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function runCommandJudgeForCwd<\n TTestCase extends BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n>(\n problemDir: string,\n cwd: string,\n params: JudgeParams,\n options: CommandJudgePresetOptions<TTestCase, TRunResult>\n): Promise<{ allAccepted: boolean }> {\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const testCases = await (options.readTestCases ?? readCommandTestCases)(problemDir);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n const limits = {\n buildTimeoutSeconds: options.limits?.buildTimeoutSeconds ?? BUILD_TIMEOUT_SECONDS,\n maxOutputLength: options.limits?.maxOutputLength ?? MAX_STDOUT_LENGTH,\n };\n const runTimeoutSeconds = options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS;\n\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return { allAccepted: false };\n }\n\n const originalMainFilePath = await findEntryPointFile(cwd, params.language);\n if (!originalMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return { allAccepted: false };\n }\n\n const languageDefinition = findLanguageDefinitionByPath(originalMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return { allAccepted: false };\n }\n\n // `CI` changes affects Chainlit. `FORCE_COLOR` affects Bun.\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let mainFilePath = originalMainFilePath;\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(cwd);\n const prebuiltMainFilePath = await findEntryPointFile(cwd, params.language);\n if (prebuiltMainFilePath) mainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: error instanceof Error ? error.message : String(error),\n });\n return { allAccepted: false };\n }\n }\n\n const buildCommand = languageDefinition.buildCommand?.(mainFilePath);\n if (buildCommand) {\n const buildResult = runBuild(buildCommand, {\n cwd,\n env,\n testCaseId: prebuildTestCaseId,\n limits,\n });\n if (buildResult) {\n printTestCaseResult(buildResult);\n return { allAccepted: false };\n }\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(cwd);\n\n if (testCases.length === 0) {\n printTestCaseResult({ testCaseId: 'default', decisionCode: DecisionCode.ACCEPTED });\n return { allAccepted: true };\n }\n\n const sharedFileInputPath = (testCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n\n for (const testCase of testCases) {\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : runTimeoutSeconds;\n\n const command = languageDefinition.command(mainFilePath);\n let stdin = testCase.input ?? '';\n let runResult: TRunResult;\n try {\n if (options.resolveInput) {\n stdin = await options.resolveInput({ testCase, cwd, env });\n }\n\n runResult = options.runCommand\n ? await options.runCommand({\n testCase,\n command,\n stdin,\n cwd,\n env,\n timeLimitSeconds,\n })\n : (runCommand(command, {\n stdin,\n cwd,\n env,\n timeLimitSeconds,\n }) as TRunResult);\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: errorToMessage(error),\n });\n await cleanWorkingDirectory(cwd, cwdSnapshot);\n return { allAccepted: false };\n }\n\n const outputFiles = await readOutputFiles(cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: CommandJudgeContext = {\n timeLimitSeconds,\n outputLimitLength: limits.maxOutputLength,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateByLimits({ runResult, outputFiles, context: judgeContext });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test?.({ testCase, runResult, outputFiles, context: judgeContext });\n if (extendedJudgeResult) {\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n };\n }\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: errorToMessage(error),\n };\n }\n }\n\n printTestCaseResult({\n testCaseId: testCase.id,\n ...judgeResult,\n exitStatus: runResult.status,\n stdin: runResult.stdin,\n stdout: runResult.stdout.slice(0, limits.maxOutputLength) || undefined,\n stderr: (judgeResult.stderr ?? runResult.stderr).slice(0, limits.maxOutputLength) || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n outputFiles: outputFiles.length > 0 ? outputFiles : undefined,\n });\n\n await cleanWorkingDirectory(cwd, cwdSnapshot);\n if (judgeResult.decisionCode !== DecisionCode.ACCEPTED) return { allAccepted: false };\n }\n\n return { allAccepted: true };\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: { allAccepted: boolean }): boolean {\n return result.allAccepted === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction runBuild(\n buildCommand: readonly [string, ...string[]],\n context: { cwd: string; env: NodeJS.ProcessEnv; testCaseId: string; limits: CommandJudgeLimits }\n): (Omit<TestCaseResult, 'testCaseId'> & { testCaseId: string }) | undefined {\n const spawnResult = spawnSyncWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, encoding: 'utf8', env: context.env },\n context.limits.buildTimeoutSeconds\n );\n const exitStatus = spawnResult.status ?? undefined;\n\n if (spawnResult.status !== 0) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n if (spawnResult.timeSeconds > context.limits.buildTimeoutSeconds) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n if (\n spawnResult.stdout.length > context.limits.maxOutputLength ||\n spawnResult.stderr.length > context.limits.maxOutputLength\n ) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n return;\n}\n\nfunction errorToMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction toCommandTestCase(value: {\n id: string;\n input?: string;\n fileInputPath?: string;\n fileOutputPath?: string;\n output?: string;\n}): BaseCommandTestCase {\n return { id: value.id, input: value.input, fileInputPath: value.fileInputPath };\n}\n\nasync function readCommandTestCases<TTestCase extends BaseCommandTestCase = BaseCommandTestCase>(\n problemDir: string\n): Promise<readonly TTestCase[]> {\n const fileTestCases = await readFileTestCases(path.join(problemDir, 'test_cases'));\n const commandTestCases = fileTestCases.map((testCase) => toCommandTestCase(testCase) as TTestCase);\n if (fileTestCases.shared?.fileInputPath) {\n return Object.assign(commandTestCases, { shared: { fileInputPath: fileTestCases.shared.fileInputPath } });\n }\n return commandTestCases;\n}\n\nfunction runCommand(\n command: readonly [string, ...string[]],\n context: {\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n }\n): CommandRunResult {\n const spawnResult = spawnSyncWithTimeout(\n command[0],\n command.slice(1),\n { cwd: context.cwd, encoding: 'utf8', input: context.stdin, env: context.env },\n context.timeLimitSeconds\n );\n\n return {\n stdin: context.stdin,\n stdout: spawnResult.stdout,\n stderr: spawnResult.stderr,\n status: spawnResult.status ?? undefined,\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n}\n\nfunction evaluateByLimits(context: {\n runResult: CommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: CommandJudgeContext;\n}): CommandJudgeCaseResult {\n if (context.runResult.status !== 0) {\n return { decisionCode: DecisionCode.RUNTIME_ERROR, stderr: context.runResult.stderr };\n }\n\n if (context.runResult.timeSeconds > context.context.timeLimitSeconds) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return { decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n if (\n context.runResult.outputLimitExceeded ||\n context.runResult.stdout.length > context.context.outputLimitLength ||\n context.runResult.stderr.length > context.context.outputLimitLength\n ) {\n return { decisionCode: DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n const requiredOutputFileCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFileCount) {\n return { decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"command.cjs","names":["z","parseArgs","resolveCwds","checkProblemDirIsolation","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","snapshotWorkingDirectory","copyTestCaseFileInput","runCustomRunner","wrapCommandWithSandboxUser","getSandboxUserEnvOverrides","cleanWorkingDirectory","readOutputFiles","spawnSyncWithTimeout","readFileTestCases","path"],"sources":["../../src/presets/command.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { z } from 'zod';\n\nimport { checkProblemDirIsolation } from '../helpers/checkProblemDirIsolation.js';\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 { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printDebugBanner } from '../helpers/printDebugBanner.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { runCustomRunner } from '../helpers/runCustomRunner.js';\nimport {\n getSandboxUserEnvOverrides,\n makeAccessibleToSandboxUser,\n wrapCommandWithSandboxUser,\n} from '../helpers/sandboxUser.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { spawnSyncWithTimeout } from '../helpers/spawnSyncWithTimeout.js';\nimport { DecisionCode } from '../types/decisionCode.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 = 2;\nconst MAX_STDOUT_LENGTH = 50_000;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\ntype JudgeParams = z.infer<typeof judgeParamsSchema>;\n\ninterface BaseCommandTestCase {\n id: string;\n input?: string;\n fileInputPath?: string;\n}\n\ntype CommandJudgeCaseResult = Pick<TestCaseResult, 'decisionCode' | 'feedbackMarkdown' | 'stderr'>;\n\nexport interface CommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded?: boolean;\n}\n\ninterface CommandJudgeContext {\n timeLimitSeconds: number;\n outputLimitLength: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\nexport interface CommandJudgeLimits {\n buildTimeoutSeconds: number;\n maxOutputLength: number;\n}\n\nexport interface CommandJudgePresetOptions<\n TTestCase extends BaseCommandTestCase = BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n> {\n limits?: CommandJudgeLimits;\n runTimeoutSeconds?: number;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n /**\n * Runs as the trusted harness user with the submission's `cwd`. Fixture files it creates there\n * must be written with `createDirectoryWithoutFollowingSymlinks`/`writeFileWithoutFollowingSymlinks`\n * (both exported): a submission can plant a symlink at a fixture path, and a plain `fs.writeFile`\n * would follow it into a file only the harness can write.\n */\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n runCommand?: (context: {\n testCase: TTestCase;\n /**\n * The command to run. Under `EXERCODE_SANDBOX_USER` delegation it is already wrapped so it\n * executes as the sandbox user; spawn it as given (with the supplied `env`) instead of\n * reconstructing it, or the submission runs as the trusted harness user.\n *\n * Its direct child is then a root-owned `sudo` whose descendants belong to the sandbox user, so\n * the handler cannot signal them: enforce `timeLimitSeconds` with `startSandboxTimeoutWatchdog`\n * and `killSandboxUserProcesses` (both exported) rather than `child.kill()` or an outer\n * `timeout`. The preset terminates leftover sandbox processes after the handler returns.\n */\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n }) => Promise<TRunResult> | TRunResult;\n test?: (context: {\n testCase: TTestCase;\n runResult: TRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: CommandJudgeContext;\n }) => Promise<Partial<CommandJudgeCaseResult>> | Partial<CommandJudgeCaseResult> | undefined;\n}\n\n/**\n * A preset function for judging by executable command.\n *\n * Keep problem-specific logic in `resolveInput` and `test`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { commandJudgePreset } from '@exercode/problem-utils/presets/command';\n * import { DecisionCode } from '@exercode/problem-utils';\n *\n * await commandJudgePreset(import.meta.dirname, {\n * readTestCases: async () => [\n * { id: '01', input: '1 2' },\n * ],\n * test: ({ runResult }) => {\n * return runResult.stdout.trim() === '3'\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n *\n * Run with the required parameters:\n * ```bash\n * bun judge.ts model_answers/python '{ \"language\": \"python\" }'\n * ```\n *\n * Run without a cwd argument to judge each `<problemDir>/model_answers/*` directory\n * for debugging. A prominent banner is printed for each run.\n * ```bash\n * bun judge.ts\n * ```\n */\nexport async function commandJudgePreset<\n TTestCase extends BaseCommandTestCase = BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n>(problemDir: string, options: CommandJudgePresetOptions<TTestCase, TRunResult>): Promise<void> {\n const args = parseArgs(process.argv);\n const params = judgeParamsSchema.parse(args.params);\n\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode) {\n const acceptedCwd = cwds.find((cwd) => cwd.expectedResult === 'accepted');\n if (acceptedCwd) {\n const isolationCheckResult = await checkProblemDirIsolation(problemDir, acceptedCwd, params);\n if (!isolationCheckResult.passed) {\n process.exitCode = 1;\n return;\n }\n } else {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'No accepted model answer is available for checking that the copied judge still accepts a valid submission.',\n ]);\n }\n }\n\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await runCommandJudgeForCwd<TTestCase, TRunResult>(problemDir, resolvedCwd.cwd, params, options);\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function runCommandJudgeForCwd<\n TTestCase extends BaseCommandTestCase,\n TRunResult extends CommandRunResult = CommandRunResult,\n>(\n problemDir: string,\n cwd: string,\n params: JudgeParams,\n options: CommandJudgePresetOptions<TTestCase, TRunResult>\n): Promise<{ allAccepted: boolean }> {\n // The sandboxed submission must read its sources and write build/run outputs in its directory.\n makeAccessibleToSandboxUser(cwd);\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const testCases = await (options.readTestCases ?? readCommandTestCases)(problemDir);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n const limits = {\n buildTimeoutSeconds: options.limits?.buildTimeoutSeconds ?? BUILD_TIMEOUT_SECONDS,\n maxOutputLength: options.limits?.maxOutputLength ?? MAX_STDOUT_LENGTH,\n };\n const runTimeoutSeconds = options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS;\n\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return { allAccepted: false };\n }\n\n const originalMainFilePath = await findEntryPointFile(cwd, params.language);\n if (!originalMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return { allAccepted: false };\n }\n\n const languageDefinition = findLanguageDefinitionByPath(originalMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return { allAccepted: false };\n }\n\n // `CI` changes affects Chainlit. `FORCE_COLOR` affects Bun.\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let mainFilePath = originalMainFilePath;\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(cwd);\n const prebuiltMainFilePath = await findEntryPointFile(cwd, params.language);\n if (prebuiltMainFilePath) mainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: error instanceof Error ? error.message : String(error),\n });\n return { allAccepted: false };\n }\n }\n\n const buildCommand = languageDefinition.buildCommand?.(mainFilePath);\n if (buildCommand) {\n const buildResult = runBuild(buildCommand, {\n cwd,\n env,\n testCaseId: prebuildTestCaseId,\n limits,\n });\n if (buildResult) {\n printTestCaseResult(buildResult);\n return { allAccepted: false };\n }\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(cwd);\n\n if (testCases.length === 0) {\n printTestCaseResult({ testCaseId: 'default', decisionCode: DecisionCode.ACCEPTED });\n return { allAccepted: true };\n }\n\n const sharedFileInputPath = (testCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n\n for (const testCase of testCases) {\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : runTimeoutSeconds;\n\n const command = languageDefinition.command(mainFilePath);\n let stdin = testCase.input ?? '';\n let runResult: TRunResult;\n try {\n if (options.resolveInput) {\n stdin = await options.resolveInput({ testCase, cwd, env });\n }\n\n runResult = options.runCommand\n ? await runCustomRunner(\n () =>\n // Hand custom runners a command that is already sandbox-wrapped, and the matching\n // environment: they spawn it themselves, so an unwrapped command would run the\n // submission as the trusted harness user and defeat the delegation boundary.\n options.runCommand?.({\n testCase,\n command: wrapCommandWithSandboxUser(command),\n stdin,\n cwd,\n env: { ...env, ...getSandboxUserEnvOverrides(env) },\n timeLimitSeconds,\n }) as Promise<TRunResult>\n )\n : (runCommand(command, {\n stdin,\n cwd,\n env,\n timeLimitSeconds,\n }) as TRunResult);\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: errorToMessage(error),\n });\n await cleanWorkingDirectory(cwd, cwdSnapshot);\n return { allAccepted: false };\n }\n\n const outputFiles = await readOutputFiles(cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: CommandJudgeContext = {\n timeLimitSeconds,\n outputLimitLength: limits.maxOutputLength,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateByLimits({ runResult, outputFiles, context: judgeContext });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test?.({ testCase, runResult, outputFiles, context: judgeContext });\n if (extendedJudgeResult) {\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n };\n }\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: errorToMessage(error),\n };\n }\n }\n\n printTestCaseResult({\n testCaseId: testCase.id,\n ...judgeResult,\n exitStatus: runResult.status,\n stdin: runResult.stdin,\n stdout: runResult.stdout.slice(0, limits.maxOutputLength) || undefined,\n stderr: (judgeResult.stderr ?? runResult.stderr).slice(0, limits.maxOutputLength) || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n outputFiles: outputFiles.length > 0 ? outputFiles : undefined,\n });\n\n await cleanWorkingDirectory(cwd, cwdSnapshot);\n if (judgeResult.decisionCode !== DecisionCode.ACCEPTED) return { allAccepted: false };\n }\n\n return { allAccepted: true };\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: { allAccepted: boolean }): boolean {\n return result.allAccepted === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction runBuild(\n buildCommand: readonly [string, ...string[]],\n context: { cwd: string; env: NodeJS.ProcessEnv; testCaseId: string; limits: CommandJudgeLimits }\n): (Omit<TestCaseResult, 'testCaseId'> & { testCaseId: string }) | undefined {\n const spawnResult = spawnSyncWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, encoding: 'utf8', env: context.env },\n context.limits.buildTimeoutSeconds\n );\n const exitStatus = spawnResult.status ?? undefined;\n\n if (spawnResult.status !== 0) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n if (spawnResult.timeSeconds > context.limits.buildTimeoutSeconds) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n if (\n spawnResult.stdout.length > context.limits.maxOutputLength ||\n spawnResult.stderr.length > context.limits.maxOutputLength\n ) {\n return {\n testCaseId: context.testCaseId,\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n exitStatus,\n stdout: spawnResult.stdout.slice(0, context.limits.maxOutputLength),\n stderr: spawnResult.stderr.slice(0, context.limits.maxOutputLength),\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n }\n\n return;\n}\n\nfunction errorToMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction toCommandTestCase(value: {\n id: string;\n input?: string;\n fileInputPath?: string;\n fileOutputPath?: string;\n output?: string;\n}): BaseCommandTestCase {\n return { id: value.id, input: value.input, fileInputPath: value.fileInputPath };\n}\n\nasync function readCommandTestCases<TTestCase extends BaseCommandTestCase = BaseCommandTestCase>(\n problemDir: string\n): Promise<readonly TTestCase[]> {\n const fileTestCases = await readFileTestCases(path.join(problemDir, 'test_cases'));\n const commandTestCases = fileTestCases.map((testCase) => toCommandTestCase(testCase) as TTestCase);\n if (fileTestCases.shared?.fileInputPath) {\n return Object.assign(commandTestCases, { shared: { fileInputPath: fileTestCases.shared.fileInputPath } });\n }\n return commandTestCases;\n}\n\nfunction runCommand(\n command: readonly [string, ...string[]],\n context: {\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n }\n): CommandRunResult {\n const spawnResult = spawnSyncWithTimeout(\n command[0],\n command.slice(1),\n { cwd: context.cwd, encoding: 'utf8', input: context.stdin, env: context.env },\n context.timeLimitSeconds\n );\n\n return {\n stdin: context.stdin,\n stdout: spawnResult.stdout,\n stderr: spawnResult.stderr,\n status: spawnResult.status ?? undefined,\n timeSeconds: spawnResult.timeSeconds,\n memoryBytes: spawnResult.memoryBytes,\n };\n}\n\nfunction evaluateByLimits(context: {\n runResult: CommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: CommandJudgeContext;\n}): CommandJudgeCaseResult {\n if (context.runResult.status !== 0) {\n return { decisionCode: DecisionCode.RUNTIME_ERROR, stderr: context.runResult.stderr };\n }\n\n if (context.runResult.timeSeconds > context.context.timeLimitSeconds) {\n return { decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return { decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n if (\n context.runResult.outputLimitExceeded ||\n context.runResult.stdout.length > context.context.outputLimitLength ||\n context.runResult.stderr.length > context.context.outputLimitLength\n ) {\n return { decisionCode: DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED, stderr: context.runResult.stderr };\n }\n\n const requiredOutputFileCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFileCount) {\n return { decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n"],"mappings":"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"}
|
|
@@ -28,6 +28,12 @@ export interface CommandJudgePresetOptions<TTestCase extends BaseCommandTestCase
|
|
|
28
28
|
limits?: CommandJudgeLimits;
|
|
29
29
|
runTimeoutSeconds?: number;
|
|
30
30
|
readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Runs as the trusted harness user with the submission's `cwd`. Fixture files it creates there
|
|
33
|
+
* must be written with `createDirectoryWithoutFollowingSymlinks`/`writeFileWithoutFollowingSymlinks`
|
|
34
|
+
* (both exported): a submission can plant a symlink at a fixture path, and a plain `fs.writeFile`
|
|
35
|
+
* would follow it into a file only the harness can write.
|
|
36
|
+
*/
|
|
31
37
|
resolveInput?: (context: {
|
|
32
38
|
testCase: TTestCase;
|
|
33
39
|
cwd: string;
|
|
@@ -35,6 +41,16 @@ export interface CommandJudgePresetOptions<TTestCase extends BaseCommandTestCase
|
|
|
35
41
|
}) => Promise<string> | string;
|
|
36
42
|
runCommand?: (context: {
|
|
37
43
|
testCase: TTestCase;
|
|
44
|
+
/**
|
|
45
|
+
* The command to run. Under `EXERCODE_SANDBOX_USER` delegation it is already wrapped so it
|
|
46
|
+
* executes as the sandbox user; spawn it as given (with the supplied `env`) instead of
|
|
47
|
+
* reconstructing it, or the submission runs as the trusted harness user.
|
|
48
|
+
*
|
|
49
|
+
* Its direct child is then a root-owned `sudo` whose descendants belong to the sandbox user, so
|
|
50
|
+
* the handler cannot signal them: enforce `timeLimitSeconds` with `startSandboxTimeoutWatchdog`
|
|
51
|
+
* and `killSandboxUserProcesses` (both exported) rather than `child.kill()` or an outer
|
|
52
|
+
* `timeout`. The preset terminates leftover sandbox processes after the handler returns.
|
|
53
|
+
*/
|
|
38
54
|
command: readonly [string, ...string[]];
|
|
39
55
|
stdin: string;
|
|
40
56
|
cwd: string;
|