hammerkit 1.27.0-next.9 → 2.0.0-next.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/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeSandboxRunners = exports.executeInRunner = exports.getExecExitCode = exports.buildRunnerPod = exports.getRunnerName = exports.getRunnerKey = exports.canUseRunner = exports.getWrapper = exports.RUNNER_CHANGED_EXIT = exports.getCleanUp = exports.getScratchDirectories = exports.getRunnerMounts = exports.RUNNER_SANDBOX_PATH = exports.IDLE_COMMAND = exports.RUNNER_TOOLS_IMAGE = exports.RUNNER_LABEL = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const string_decoder_1 = require("string_decoder");
|
|
7
|
+
const stream_1 = require("stream");
|
|
8
|
+
const promises_1 = require("stream/promises");
|
|
9
|
+
const work_network_1 = require("../planner/work-network");
|
|
10
|
+
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
11
|
+
const log_1 = require("../log");
|
|
12
|
+
const sleep_1 = require("../utils/sleep");
|
|
13
|
+
const version_1 = require("../version");
|
|
14
|
+
const apply_1 = require("./apply");
|
|
15
|
+
const container_resources_1 = require("./container-resources");
|
|
16
|
+
const job_placement_1 = require("./job-placement");
|
|
17
|
+
const network_policy_1 = require("./network-policy");
|
|
18
|
+
const resources_1 = require("./resources");
|
|
19
|
+
const volumes_1 = require("./volumes");
|
|
20
|
+
/**
|
|
21
|
+
* The runner of a sandbox: one pod that the commands of a run on the sandbox are exec'd
|
|
22
|
+
* into, one after the other, in place of a Job each. A Job's pod takes seconds to be
|
|
23
|
+
* scheduled and to start; an exec into a running pod a fraction of one.
|
|
24
|
+
*
|
|
25
|
+
* - Only a task shaped like an agent's shell gets it: a container task that mounts the
|
|
26
|
+
* whole sandbox (`mounts: [.]`) and nothing else (no claims of its own or of its
|
|
27
|
+
* dependencies, so no `generates`), with no `needs` and no secrets. Any other task runs
|
|
28
|
+
* as a Job.
|
|
29
|
+
* - The pod is the task's container as its Job would have it: its image, variables,
|
|
30
|
+
* resources, the sandbox's claim, the security context, the nodes of the pool and the
|
|
31
|
+
* node of the pod of the sandbox's files. It is named after all of that (getRunnerKey),
|
|
32
|
+
* so a change starts a new one and the one before goes. Its first process only waits.
|
|
33
|
+
* - Every command runs in a wrapper that first and last kills every other process of the
|
|
34
|
+
* container and empties /tmp and /dev/shm (unless a mount of the task is in one): the
|
|
35
|
+
* container is reused, what a command started is not. A command cancelled, out of time or whose connection broke is killed
|
|
36
|
+
* the same way, or the pod is deleted. The first process and the wrapper run a static
|
|
37
|
+
* busybox from a read-only mount, so a command can not replace what cleans up after it.
|
|
38
|
+
* - Under a limited network it has a NetworkPolicy of its own and, under `allow`, the
|
|
39
|
+
* sandbox's egress proxy, both keyed like the pod. They carry no run, and go with the
|
|
40
|
+
* pod of the sandbox's files, which owns them and the runner: the runner stays while
|
|
41
|
+
* the sandbox has that pod, `backend.sandboxes.idle` after its last run or request.
|
|
42
|
+
*/
|
|
43
|
+
exports.RUNNER_LABEL = 'hammerkit.dev/runner';
|
|
44
|
+
const RUNNER_CONTAINER = 'runner';
|
|
45
|
+
const TMP_VOLUME = 'hammerkit-runner-tmp';
|
|
46
|
+
const TOOLS_VOLUME = 'hammerkit-runner-tools';
|
|
47
|
+
const TOOLS_PATH = '/.hammerkit';
|
|
48
|
+
const BUSYBOX = `${TOOLS_PATH}/busybox`;
|
|
49
|
+
// Pinned, static (it runs in any image), and copied into the runner by an init container.
|
|
50
|
+
// To bump it, take the index digest of the tag (`docker buildx imagetools inspect
|
|
51
|
+
// busybox:<version>-musl`).
|
|
52
|
+
exports.RUNNER_TOOLS_IMAGE = 'busybox:1.37.0-musl@sha256:5cec3fc171c87218698e85a52af7087de727372aae264a787b8112901a5b0092';
|
|
53
|
+
const RUNNER_START_MS = 5 * 60 * 1000;
|
|
54
|
+
const RUNNER_POLL_MS = 250;
|
|
55
|
+
const PULL_FAILURES = ['ErrImagePull', 'ImagePullBackOff', 'InvalidImageName'];
|
|
56
|
+
const KILL_TIMEOUT_MS = 10000;
|
|
57
|
+
// a command's output is kept to this, as the log of a Job
|
|
58
|
+
const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
59
|
+
// Waits without a process of its own to signal: a command's `kill -KILL -1` ends the
|
|
60
|
+
// sleep, not the loop (the first process of the container is spared), which reaps what
|
|
61
|
+
// the commands left
|
|
62
|
+
exports.IDLE_COMMAND = [BUSYBOX, 'sh', '-c', `trap "exit 0" TERM; while :; do ${BUSYBOX} sleep 3600 & wait; done`];
|
|
63
|
+
const SCRATCH_DIRECTORIES = ['/tmp', '/dev/shm'];
|
|
64
|
+
// Where the runner mounts the sandbox's files. A run has them in a directory of its own (the
|
|
65
|
+
// server's directory of the run), which every command links here first: the pod and its key
|
|
66
|
+
// stay the same from one run to the next.
|
|
67
|
+
exports.RUNNER_SANDBOX_PATH = '/.hammerkit-sandbox';
|
|
68
|
+
// The mounts of the task, from the directory of the run to the runner's
|
|
69
|
+
function getRunnerMounts(workspace, persistence) {
|
|
70
|
+
return persistence.mounts.map(({ mount }) => ({ ...mount, mountPath: toRunnerPath(workspace, mount.mountPath) }));
|
|
71
|
+
}
|
|
72
|
+
exports.getRunnerMounts = getRunnerMounts;
|
|
73
|
+
function toRunnerPath(workspace, path) {
|
|
74
|
+
const rel = (0, path_1.relative)(workspace.root, path);
|
|
75
|
+
return rel === '' ? exports.RUNNER_SANDBOX_PATH : path_1.posix.join(exports.RUNNER_SANDBOX_PATH, ...rel.split(path_1.sep));
|
|
76
|
+
}
|
|
77
|
+
// The scratch directories the runner empties: not one that holds a mount of the task, or is
|
|
78
|
+
// in one, such as the sandbox's files when the server keeps its runs below /tmp
|
|
79
|
+
function getScratchDirectories(mounts) {
|
|
80
|
+
const within = (path, directory) => path === directory || path.startsWith(`${directory}/`);
|
|
81
|
+
return SCRATCH_DIRECTORIES.filter((directory) => mounts.every((mount) => !within(mount.mountPath, directory) && !within(directory, mount.mountPath)));
|
|
82
|
+
}
|
|
83
|
+
exports.getScratchDirectories = getScratchDirectories;
|
|
84
|
+
// Kills every process of the container but the first and the shell itself (`kill` is the
|
|
85
|
+
// shell's own), again while one forked meanwhile, then empties the scratch directories
|
|
86
|
+
function getCleanUp(directories) {
|
|
87
|
+
const patterns = directories.flatMap((directory) => [`${directory}/*`, `${directory}/.[!.]*`, `${directory}/..?*`]);
|
|
88
|
+
return [
|
|
89
|
+
'i=0',
|
|
90
|
+
'while kill -KILL -1 2>/dev/null && [ $i -lt 10 ]; do i=$((i+1)); done',
|
|
91
|
+
...(patterns.length > 0 ? [`${BUSYBOX} rm -rf ${patterns.join(' ')} 2>/dev/null`] : []),
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
exports.getCleanUp = getCleanUp;
|
|
95
|
+
// What the wrapper says on stderr, with RUNNER_CHANGED_EXIT, when a command before changed
|
|
96
|
+
// the runner: the command did not run, the runner goes and the task fails
|
|
97
|
+
exports.RUNNER_CHANGED_EXIT = 125;
|
|
98
|
+
const RUNNER_CHANGED_MARK = `hammerkit-runner-changed-${(0, crypto_1.randomBytes)(8).toString('hex')}`;
|
|
99
|
+
// Runs `"$@"` in the directory `$3`, alone in the container: what an earlier command left
|
|
100
|
+
// is cleaned up before, what this one left after. The directory of the run `$2` is a link
|
|
101
|
+
// to the sandbox's files first. Before the command runs, each of `mountPaths` must still be
|
|
102
|
+
// the mount it was made as (the kernel's mountinfo, which no command writes): a command
|
|
103
|
+
// before could have moved a folder a mount is in and put files of its own in its place.
|
|
104
|
+
// When not, it says `$1` and exits RUNNER_CHANGED_EXIT. Else its exit code is the command's.
|
|
105
|
+
function getWrapper(cleanUp, mountPaths) {
|
|
106
|
+
const sandbox = shellQuote(exports.RUNNER_SANDBOX_PATH);
|
|
107
|
+
return [
|
|
108
|
+
cleanUp,
|
|
109
|
+
`changed() { echo "$1" >&2; exit ${exports.RUNNER_CHANGED_EXIT}; }`,
|
|
110
|
+
`${BUSYBOX} mkdir -p "$(${BUSYBOX} dirname "$2")" && ${BUSYBOX} ln -sfn ${sandbox} "$2" || changed "$1"`,
|
|
111
|
+
...mountPaths.map((path) => {
|
|
112
|
+
const quoted = shellQuote(path);
|
|
113
|
+
const field = shellQuote(`^[^ ]* [^ ]* [^ ]* [^ ]* ${toMountInfoPattern(path)} `);
|
|
114
|
+
return `[ "$(${BUSYBOX} realpath ${quoted})" = ${quoted} ] && ${BUSYBOX} grep -q ${field} /proc/self/mountinfo || changed "$1"`;
|
|
115
|
+
}),
|
|
116
|
+
'cd "$3" || exit 126',
|
|
117
|
+
`case "$(pwd -P)" in ${sandbox}|${sandbox}/*) ;; *) changed "$1" ;; esac`,
|
|
118
|
+
'shift 3',
|
|
119
|
+
'"$@"',
|
|
120
|
+
'code=$?',
|
|
121
|
+
cleanUp,
|
|
122
|
+
'exit $code',
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
exports.getWrapper = getWrapper;
|
|
126
|
+
function shellQuote(value) {
|
|
127
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
128
|
+
}
|
|
129
|
+
// A mount point as /proc/self/mountinfo writes it (space, tab, newline and backslash as
|
|
130
|
+
// octal escapes), as a basic regular expression
|
|
131
|
+
function toMountInfoPattern(path) {
|
|
132
|
+
const escaped = path.replace(/[\\ \t\n]/g, (c) => `\\${c.charCodeAt(0).toString(8).padStart(3, '0')}`);
|
|
133
|
+
return escaped.replace(/[.[\]*^$\\]/g, (c) => `\\${c}`);
|
|
134
|
+
}
|
|
135
|
+
// the sandboxes whose runner a task of this process uses: one command at a time each, the
|
|
136
|
+
// other tasks of the run run as Jobs meanwhile
|
|
137
|
+
const inUse = new Set();
|
|
138
|
+
// per sandbox, the runners whose container ended as it started (an image without `sh`, or
|
|
139
|
+
// one that can not be pulled): their tasks run as Jobs while that pod of the files stays
|
|
140
|
+
const failed = new Map();
|
|
141
|
+
// Whether the task's commands may run in the sandbox's runner
|
|
142
|
+
function canUseRunner(task, kubernetes, persistence) {
|
|
143
|
+
const workspace = kubernetes.server?.workspace;
|
|
144
|
+
return (!!workspace?.runner &&
|
|
145
|
+
!!workspace.pod &&
|
|
146
|
+
persistence.workspace.length > 0 &&
|
|
147
|
+
persistence.volumes.length === 0 &&
|
|
148
|
+
persistence.mounts.every((mount) => mount.workspace) &&
|
|
149
|
+
// the whole of the files at the root, so the runner has no writable directory of its own
|
|
150
|
+
// around the mounts that a command could leave files in for the next runs
|
|
151
|
+
persistence.mounts.some((mount) => (0, path_1.relative)(workspace.root, mount.mount.mountPath) === '') &&
|
|
152
|
+
task.needs.length === 0 &&
|
|
153
|
+
task.data.secrets.length === 0 &&
|
|
154
|
+
task.data.generates.length === 0 &&
|
|
155
|
+
// its pod would need the sandboxed runtime of a Job
|
|
156
|
+
task.data.capAdd.length === 0);
|
|
157
|
+
}
|
|
158
|
+
exports.canUseRunner = canUseRunner;
|
|
159
|
+
// What the runner is made of: the same key for the same pod
|
|
160
|
+
function getRunnerKey(task, kubernetes, persistence) {
|
|
161
|
+
const server = kubernetes.server;
|
|
162
|
+
const workspace = server.workspace;
|
|
163
|
+
return (0, crypto_1.createHash)('sha256')
|
|
164
|
+
.update(JSON.stringify({
|
|
165
|
+
sandbox: workspace.label,
|
|
166
|
+
pod: workspace.pod?.uid ?? null,
|
|
167
|
+
image: task.data.image,
|
|
168
|
+
envs: (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs),
|
|
169
|
+
resources: (0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
170
|
+
network: (0, work_network_1.getEffectiveNetwork)(task.data),
|
|
171
|
+
mounts: getRunnerMounts(workspace, persistence),
|
|
172
|
+
placement: (0, job_placement_1.getPodPlacement)(server, task.data.pool),
|
|
173
|
+
security: (0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
174
|
+
version: (0, version_1.getVersion)(),
|
|
175
|
+
}))
|
|
176
|
+
.digest('hex')
|
|
177
|
+
.slice(0, 40);
|
|
178
|
+
}
|
|
179
|
+
exports.getRunnerKey = getRunnerKey;
|
|
180
|
+
function getRunnerName(key) {
|
|
181
|
+
return `hammerkit-runner-${key.slice(0, 20)}`;
|
|
182
|
+
}
|
|
183
|
+
exports.getRunnerName = getRunnerName;
|
|
184
|
+
function buildRunnerPod(options) {
|
|
185
|
+
const { task, kubernetes, persistence, key, network, owner } = options;
|
|
186
|
+
const server = kubernetes.server;
|
|
187
|
+
const workspace = server.workspace;
|
|
188
|
+
const placement = (0, job_placement_1.getPodPlacement)(server, task.data.pool);
|
|
189
|
+
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
|
|
190
|
+
return {
|
|
191
|
+
apiVersion: 'v1',
|
|
192
|
+
kind: 'Pod',
|
|
193
|
+
metadata: {
|
|
194
|
+
namespace: kubernetes.namespace,
|
|
195
|
+
name: getRunnerName(key),
|
|
196
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
197
|
+
labels: {
|
|
198
|
+
// what its policy and proxy select it by
|
|
199
|
+
[resources_1.ID_LABEL]: key,
|
|
200
|
+
[exports.RUNNER_LABEL]: key.slice(0, 20),
|
|
201
|
+
[volumes_1.SANDBOX_LABEL]: workspace.label,
|
|
202
|
+
...(0, network_policy_1.getNetworkLabels)(kubernetes, task),
|
|
203
|
+
},
|
|
204
|
+
ownerReferences: [owner],
|
|
205
|
+
},
|
|
206
|
+
spec: {
|
|
207
|
+
automountServiceAccountToken: false,
|
|
208
|
+
enableServiceLinks: false,
|
|
209
|
+
restartPolicy: 'Never',
|
|
210
|
+
terminationGracePeriodSeconds: 0,
|
|
211
|
+
nodeSelector: placement.nodeSelector,
|
|
212
|
+
...(placement.tolerations.length > 0 ? { tolerations: placement.tolerations } : {}),
|
|
213
|
+
affinity: (0, volumes_1.getSandboxAffinity)(workspace.label),
|
|
214
|
+
hostAliases: network.hostAliases,
|
|
215
|
+
initContainers: [
|
|
216
|
+
...network.initContainers,
|
|
217
|
+
{
|
|
218
|
+
name: 'hammerkit-runner-tools',
|
|
219
|
+
image: exports.RUNNER_TOOLS_IMAGE,
|
|
220
|
+
command: ['/bin/busybox', 'cp', '/bin/busybox', BUSYBOX],
|
|
221
|
+
volumeMounts: [{ name: TOOLS_VOLUME, mountPath: TOOLS_PATH }],
|
|
222
|
+
securityContext: {
|
|
223
|
+
runAsNonRoot: true,
|
|
224
|
+
runAsUser: 65534,
|
|
225
|
+
allowPrivilegeEscalation: false,
|
|
226
|
+
readOnlyRootFilesystem: true,
|
|
227
|
+
capabilities: { drop: ['ALL'] },
|
|
228
|
+
},
|
|
229
|
+
resources: { limits: { memory: '16Mi' } },
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
containers: [
|
|
233
|
+
{
|
|
234
|
+
name: RUNNER_CONTAINER,
|
|
235
|
+
image: task.data.image,
|
|
236
|
+
command: exports.IDLE_COMMAND,
|
|
237
|
+
workingDir: exports.RUNNER_SANDBOX_PATH,
|
|
238
|
+
env: [...Object.entries(envs).map(([name, value]) => ({ name, value })), ...network.env],
|
|
239
|
+
volumeMounts: [
|
|
240
|
+
...getRunnerMounts(workspace, persistence),
|
|
241
|
+
{ name: TMP_VOLUME, mountPath: '/tmp' },
|
|
242
|
+
// not even root in the container writes it: it has no capability to remount
|
|
243
|
+
{ name: TOOLS_VOLUME, mountPath: TOOLS_PATH, readOnly: true },
|
|
244
|
+
],
|
|
245
|
+
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
246
|
+
...(0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
volumes: [...persistence.workspace, { name: TMP_VOLUME, emptyDir: {} }, { name: TOOLS_VOLUME, emptyDir: {} }],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
exports.buildRunnerPod = buildRunnerPod;
|
|
254
|
+
// The exit code of an exec, from the status the API server ends it with
|
|
255
|
+
function getExecExitCode(status) {
|
|
256
|
+
if (status?.status === 'Success') {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
const code = status?.details?.causes?.find((cause) => cause.reason === 'ExitCode')?.message;
|
|
260
|
+
return code !== undefined && /^\d+$/.test(code) ? Number(code) : undefined;
|
|
261
|
+
}
|
|
262
|
+
exports.getExecExitCode = getExecExitCode;
|
|
263
|
+
/**
|
|
264
|
+
* Runs the task's commands in the sandbox's runner, started when there is none (or one of
|
|
265
|
+
* something else). `unavailable` when the task can not use it now (the runner is in use by
|
|
266
|
+
* another task of the run, the pod of the sandbox's files went, or the runner did not
|
|
267
|
+
* start): the task then runs as Jobs.
|
|
268
|
+
*/
|
|
269
|
+
async function executeInRunner(instance, task, kubernetes, persistence, abort) {
|
|
270
|
+
const workspace = kubernetes.server.workspace;
|
|
271
|
+
if (inUse.has(workspace.label)) {
|
|
272
|
+
return { type: 'unavailable' };
|
|
273
|
+
}
|
|
274
|
+
inUse.add(workspace.label);
|
|
275
|
+
try {
|
|
276
|
+
const filesPod = await getFilesPod(instance, kubernetes.namespace, workspace);
|
|
277
|
+
if (!filesPod) {
|
|
278
|
+
return { type: 'unavailable' };
|
|
279
|
+
}
|
|
280
|
+
const key = getRunnerKey(task, kubernetes, persistence);
|
|
281
|
+
const name = getRunnerName(key);
|
|
282
|
+
const failedOf = failed.get(workspace.label);
|
|
283
|
+
if (failedOf?.pod === filesPod.uid && failedOf.keys.has(key)) {
|
|
284
|
+
return { type: 'unavailable' };
|
|
285
|
+
}
|
|
286
|
+
// its policy and proxy select it by the key, and stay for the next runs
|
|
287
|
+
const item = { ...task, id: () => key };
|
|
288
|
+
const found = await getRunner(instance, kubernetes.namespace, name, filesPod.uid);
|
|
289
|
+
let runner = found?.ready ? found.pod : undefined;
|
|
290
|
+
const sandbox = { label: workspace.label, pod: workspace.pod };
|
|
291
|
+
const owners = (pod) => [filesPod, ...(pod ? [ownerOf(pod)] : [])];
|
|
292
|
+
const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, item, {
|
|
293
|
+
followLog: true,
|
|
294
|
+
run: null,
|
|
295
|
+
sandbox,
|
|
296
|
+
owners: owners(runner),
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
if (!runner) {
|
|
300
|
+
let started;
|
|
301
|
+
if (found) {
|
|
302
|
+
// one a task before started, which stopped while it did: it goes on starting
|
|
303
|
+
started = await waitForRunner(instance, kubernetes.namespace, found.pod, abort);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// one of something else goes, with its limits
|
|
307
|
+
await removeSandboxRunners(instance, kubernetes.namespace, workspace.label, (pod) => pod.metadata?.name === name);
|
|
308
|
+
started = await startRunner(instance, buildRunnerPod({ task, kubernetes, persistence, key, network, owner: filesPod }), abort);
|
|
309
|
+
}
|
|
310
|
+
if (!('pod' in started)) {
|
|
311
|
+
if (started.reason === 'ended') {
|
|
312
|
+
if (failedOf?.pod !== filesPod.uid) {
|
|
313
|
+
failed.set(workspace.label, { pod: filesPod.uid, keys: new Set() });
|
|
314
|
+
}
|
|
315
|
+
failed.get(workspace.label).keys.add(key);
|
|
316
|
+
}
|
|
317
|
+
task.status.write('debug', `the runner of the sandbox did not start (${started.message}), the task runs as a Job`);
|
|
318
|
+
return { type: 'unavailable' };
|
|
319
|
+
}
|
|
320
|
+
runner = started.pod;
|
|
321
|
+
if (Object.keys((0, network_policy_1.getNetworkLabels)(kubernetes, task)).length > 0) {
|
|
322
|
+
// the policy stays until the runner ended too
|
|
323
|
+
await setPolicyOwners(instance, kubernetes.namespace, (0, network_policy_1.getNetworkPolicyName)(item, { run: null }), owners(runner));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
task.status.write('debug', `runs in the runner ${name} of the sandbox`);
|
|
328
|
+
}
|
|
329
|
+
const mounts = getRunnerMounts(workspace, persistence);
|
|
330
|
+
const cleanUp = getCleanUp(getScratchDirectories(mounts));
|
|
331
|
+
const wrapper = getWrapper(cleanUp, mounts.map((mount) => mount.mountPath));
|
|
332
|
+
for (const cmd of task.data.cmds) {
|
|
333
|
+
task.status.write('debug', `execute ${cmd.cmd} in the runner of the sandbox`);
|
|
334
|
+
const exitCode = await execInRunner(instance, kubernetes.namespace, name, workspace.root, cmd.cwd, [task.data.shell, '-c', cmd.cmd], { wrapper, cleanUp }, task, abort);
|
|
335
|
+
if (abort.aborted) {
|
|
336
|
+
return { type: 'canceled' };
|
|
337
|
+
}
|
|
338
|
+
if (exitCode === 'changed') {
|
|
339
|
+
// a fresh pod for the next task; this one fails rather than running again as a Job,
|
|
340
|
+
// since a command can say so itself after it ran
|
|
341
|
+
await removeSandboxRunners(instance, kubernetes.namespace, workspace.label, (pod) => pod.metadata?.name !== name);
|
|
342
|
+
throw new Error('a command changed the runner of the sandbox, it was removed: run the task again');
|
|
343
|
+
}
|
|
344
|
+
if (exitCode !== 0) {
|
|
345
|
+
return { type: 'crash', exitCode };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { type: 'done' };
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
network.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
inUse.delete(workspace.label);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.executeInRunner = executeInRunner;
|
|
359
|
+
// The pod of the sandbox's files the run holds, as long as it is still that pod
|
|
360
|
+
async function getFilesPod(instance, namespace, workspace) {
|
|
361
|
+
if (!workspace.pod) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const pod = await instance.coreApi
|
|
365
|
+
.readNamespacedPod({ name: workspace.pod.name, namespace })
|
|
366
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
367
|
+
if (!pod || pod.metadata?.uid !== workspace.pod.uid || pod.metadata?.deletionTimestamp) {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
return ownerOf(pod);
|
|
371
|
+
}
|
|
372
|
+
function ownerOf(pod) {
|
|
373
|
+
return { apiVersion: 'v1', kind: 'Pod', name: pod.metadata.name, uid: pod.metadata.uid };
|
|
374
|
+
}
|
|
375
|
+
// The runner of that name, when it belongs to the pod of the files and runs, or still
|
|
376
|
+
// starts (`ready` false); one that ended or is going is deleted
|
|
377
|
+
async function getRunner(instance, namespace, name, owner) {
|
|
378
|
+
const pod = await instance.coreApi
|
|
379
|
+
.readNamespacedPod({ name, namespace })
|
|
380
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
381
|
+
if (!pod) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const ours = !pod.metadata?.deletionTimestamp &&
|
|
385
|
+
(pod.metadata?.ownerReferences ?? []).some((reference) => reference.uid === owner);
|
|
386
|
+
if (ours && isRunning(pod)) {
|
|
387
|
+
return { pod, ready: true };
|
|
388
|
+
}
|
|
389
|
+
if (ours && pod.status?.phase === 'Pending' && !getStartFailure(pod)) {
|
|
390
|
+
return { pod, ready: false };
|
|
391
|
+
}
|
|
392
|
+
await deletePod(instance, namespace, name);
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
function isRunning(pod) {
|
|
396
|
+
const status = pod.status?.containerStatuses?.find((s) => s.name === RUNNER_CONTAINER);
|
|
397
|
+
return pod.status?.phase === 'Running' && !!status?.state?.running;
|
|
398
|
+
}
|
|
399
|
+
// Why the runner will not run: its container ended, or an image can not be pulled, which
|
|
400
|
+
// waits forever and is as good as ended
|
|
401
|
+
function getStartFailure(pod) {
|
|
402
|
+
const status = pod.status?.containerStatuses?.find((s) => s.name === RUNNER_CONTAINER);
|
|
403
|
+
const unpulled = [...(pod.status?.initContainerStatuses ?? []), ...(pod.status?.containerStatuses ?? [])].find((s) => PULL_FAILURES.includes(s.state?.waiting?.reason ?? ''));
|
|
404
|
+
if (unpulled) {
|
|
405
|
+
return `${unpulled.state?.waiting?.reason} of ${unpulled.image}`;
|
|
406
|
+
}
|
|
407
|
+
if (pod.status?.phase === 'Failed' || pod.status?.phase === 'Succeeded' || status?.state?.terminated) {
|
|
408
|
+
return `its container ended with ${status?.state?.terminated?.exitCode ?? 'no code'}`;
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
// Waits until no pod of that name is left: a new one of the same name is refused before
|
|
413
|
+
async function deletePod(instance, namespace, name) {
|
|
414
|
+
try {
|
|
415
|
+
await instance.coreApi.deleteNamespacedPod({ name, namespace, gracePeriodSeconds: 0 });
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
throw e;
|
|
422
|
+
}
|
|
423
|
+
const deadline = Date.now() + RUNNER_START_MS;
|
|
424
|
+
while (Date.now() < deadline) {
|
|
425
|
+
const left = await instance.coreApi
|
|
426
|
+
.readNamespacedPod({ name, namespace })
|
|
427
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
428
|
+
if (!left) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
await (0, sleep_1.sleep)(RUNNER_POLL_MS);
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`the runner ${name} is still there`);
|
|
434
|
+
}
|
|
435
|
+
// Starts the runner and answers it once it runs, or why it does not: the cluster refused
|
|
436
|
+
// it (a full quota, say, which a Job waits for), its container ended, or it took too long
|
|
437
|
+
async function startRunner(instance, spec, abort) {
|
|
438
|
+
try {
|
|
439
|
+
await instance.objectApi.create(spec);
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
return { reason: 'refused', message: (0, log_1.getErrorMessage)(e) };
|
|
443
|
+
}
|
|
444
|
+
return await waitForRunner(instance, spec.metadata.namespace, spec, abort);
|
|
445
|
+
}
|
|
446
|
+
// Waits until the runner runs. A task stopped meanwhile (cancelled, out of time) leaves it
|
|
447
|
+
// starting, for the next command, which would time out the same way if each started anew;
|
|
448
|
+
// one that ends, or does not run within RUNNER_START_MS of being made, goes.
|
|
449
|
+
async function waitForRunner(instance, namespace, runner, abort) {
|
|
450
|
+
const name = runner.metadata.name;
|
|
451
|
+
const created = runner.metadata?.creationTimestamp ? new Date(runner.metadata.creationTimestamp).getTime() : NaN;
|
|
452
|
+
const deadline = (Number.isNaN(created) ? Date.now() : Math.min(created, Date.now())) + RUNNER_START_MS;
|
|
453
|
+
for (;;) {
|
|
454
|
+
const pod = await instance.coreApi.readNamespacedPod({ name, namespace });
|
|
455
|
+
if (isRunning(pod)) {
|
|
456
|
+
return { pod };
|
|
457
|
+
}
|
|
458
|
+
const failure = getStartFailure(pod);
|
|
459
|
+
if (failure || Date.now() > deadline) {
|
|
460
|
+
await deletePod(instance, namespace, name).catch(() => undefined);
|
|
461
|
+
return failure ? { reason: 'ended', message: failure } : { reason: 'stopped', message: 'it took too long' };
|
|
462
|
+
}
|
|
463
|
+
if (abort.aborted) {
|
|
464
|
+
return { reason: 'stopped', message: 'the task was stopped' };
|
|
465
|
+
}
|
|
466
|
+
await (0, sleep_1.sleep)(RUNNER_POLL_MS);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Removes the runners of a sandbox but those `keep` takes, and once each is gone its policy
|
|
470
|
+
// and proxy, which the pod of the sandbox's files would keep while it runs
|
|
471
|
+
async function removeSandboxRunners(instance, namespace, sandbox, keep) {
|
|
472
|
+
if (!keep) {
|
|
473
|
+
// all of the sandbox's runners go: so does what was remembered of them
|
|
474
|
+
failed.delete(sandbox);
|
|
475
|
+
}
|
|
476
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
477
|
+
namespace,
|
|
478
|
+
labelSelector: `${volumes_1.SANDBOX_LABEL}=${sandbox},${exports.RUNNER_LABEL}`,
|
|
479
|
+
});
|
|
480
|
+
for (const pod of pods.items) {
|
|
481
|
+
const name = pod.metadata?.name;
|
|
482
|
+
const key = pod.metadata?.labels?.[resources_1.ID_LABEL];
|
|
483
|
+
if (!name || keep?.(pod)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
await deletePod(instance, namespace, name);
|
|
487
|
+
if (key) {
|
|
488
|
+
const item = { id: () => key };
|
|
489
|
+
await instance.objectApi
|
|
490
|
+
.delete({
|
|
491
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
492
|
+
kind: 'NetworkPolicy',
|
|
493
|
+
metadata: { name: (0, network_policy_1.getNetworkPolicyName)(item, { run: null }), namespace },
|
|
494
|
+
})
|
|
495
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
496
|
+
await (0, network_policy_1.removeSandboxProxies)(instance, namespace, { sandbox, item });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
exports.removeSandboxRunners = removeSandboxRunners;
|
|
501
|
+
async function setPolicyOwners(instance, namespace, name, owners) {
|
|
502
|
+
await instance.objectApi.patch({
|
|
503
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
504
|
+
kind: 'NetworkPolicy',
|
|
505
|
+
metadata: { name, namespace, ownerReferences: owners },
|
|
506
|
+
}, undefined, undefined, undefined, undefined, 'application/merge-patch+json');
|
|
507
|
+
}
|
|
508
|
+
// Runs a command in the runner and answers its exit code. Its output goes to the task's,
|
|
509
|
+
// line by line as it comes. On abort (cancel, timeout) every process of the command is
|
|
510
|
+
// killed, or the runner goes when that fails.
|
|
511
|
+
async function execInRunner(instance, namespace, pod,
|
|
512
|
+
// the directory of the run's sandbox, and the command's in it
|
|
513
|
+
root, cwd, command, scripts, task, abort) {
|
|
514
|
+
const { cleanUp } = scripts;
|
|
515
|
+
const output = { bytes: 0, cut: false };
|
|
516
|
+
let changed = false;
|
|
517
|
+
const lines = (stream) => {
|
|
518
|
+
let rest = '';
|
|
519
|
+
// a character split between two chunks stays whole
|
|
520
|
+
const decoder = new string_decoder_1.StringDecoder('utf8');
|
|
521
|
+
const writable = new stream_1.Writable({
|
|
522
|
+
write(chunk, _encoding, callback) {
|
|
523
|
+
if (output.bytes < MAX_OUTPUT_BYTES) {
|
|
524
|
+
output.bytes += chunk.length;
|
|
525
|
+
const parts = (rest + decoder.write(chunk)).split(/\r?\n/);
|
|
526
|
+
rest = parts.pop() ?? '';
|
|
527
|
+
for (const line of parts) {
|
|
528
|
+
if (stream === 'stderr' && line === RUNNER_CHANGED_MARK) {
|
|
529
|
+
changed = true;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
task.status.console(stream, line);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else if (!output.cut) {
|
|
536
|
+
output.cut = true;
|
|
537
|
+
task.status.write('warn', `the output of ${task.name} was cut at ${MAX_OUTPUT_BYTES} bytes`);
|
|
538
|
+
}
|
|
539
|
+
callback();
|
|
540
|
+
},
|
|
541
|
+
final(callback) {
|
|
542
|
+
rest += decoder.end();
|
|
543
|
+
if (rest.length > 0) {
|
|
544
|
+
task.status.console(stream, rest);
|
|
545
|
+
}
|
|
546
|
+
callback();
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
return writable;
|
|
550
|
+
};
|
|
551
|
+
const stdout = lines('stdout');
|
|
552
|
+
const stderr = lines('stderr');
|
|
553
|
+
let status;
|
|
554
|
+
const socket = await instance.exec.exec(namespace, pod, RUNNER_CONTAINER, [BUSYBOX, 'sh', '-c', scripts.wrapper, 'hammerkit', RUNNER_CHANGED_MARK, root, cwd, ...command], stdout, stderr, null, false, (received) => {
|
|
555
|
+
status = received;
|
|
556
|
+
});
|
|
557
|
+
const closed = new Promise((resolve, reject) => {
|
|
558
|
+
socket.on('close', () => resolve());
|
|
559
|
+
socket.on('error', (e) => reject(e));
|
|
560
|
+
});
|
|
561
|
+
const kill = async () => {
|
|
562
|
+
try {
|
|
563
|
+
await killInRunner(instance, namespace, pod, cleanUp);
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
task.status.write('debug', `the command could not be stopped in the runner, it goes: ${(0, log_1.getErrorMessage)(e)}`);
|
|
567
|
+
await deletePod(instance, namespace, pod).catch(() => undefined);
|
|
568
|
+
}
|
|
569
|
+
// what the command printed after it was killed is not waited for long
|
|
570
|
+
await Promise.race([closed.catch(() => undefined), (0, sleep_1.sleep)(KILL_TIMEOUT_MS)]);
|
|
571
|
+
socket.close();
|
|
572
|
+
};
|
|
573
|
+
const aborted = () => {
|
|
574
|
+
void kill();
|
|
575
|
+
};
|
|
576
|
+
if (abort.aborted) {
|
|
577
|
+
aborted();
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
abort.addEventListener('abort', aborted, { once: true });
|
|
581
|
+
}
|
|
582
|
+
let broken;
|
|
583
|
+
try {
|
|
584
|
+
await closed.catch((e) => {
|
|
585
|
+
broken = e;
|
|
586
|
+
});
|
|
587
|
+
if (!abort.aborted && getExecExitCode(status) === undefined) {
|
|
588
|
+
// the connection broke while the command may still run: it ends here as on a cancel,
|
|
589
|
+
// before the run lets go of the sandbox
|
|
590
|
+
await kill();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
abort.removeEventListener('abort', aborted);
|
|
595
|
+
for (const stream of [stdout, stderr]) {
|
|
596
|
+
if (!stream.writableEnded) {
|
|
597
|
+
stream.end();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
await Promise.all([(0, promises_1.finished)(stdout), (0, promises_1.finished)(stderr)]).catch(() => undefined);
|
|
601
|
+
}
|
|
602
|
+
if (abort.aborted) {
|
|
603
|
+
return 137;
|
|
604
|
+
}
|
|
605
|
+
const exitCode = getExecExitCode(status);
|
|
606
|
+
if (exitCode === undefined) {
|
|
607
|
+
throw new Error(`the command in the runner of the sandbox ended without an exit code: ${status?.message ?? (broken ? (0, log_1.getErrorMessage)(broken) : 'the connection closed')}`);
|
|
608
|
+
}
|
|
609
|
+
return exitCode === exports.RUNNER_CHANGED_EXIT && changed ? 'changed' : exitCode;
|
|
610
|
+
}
|
|
611
|
+
async function killInRunner(instance, namespace, pod, cleanUp) {
|
|
612
|
+
let status;
|
|
613
|
+
const socket = await instance.exec.exec(namespace, pod, RUNNER_CONTAINER, [BUSYBOX, 'sh', '-c', cleanUp], null, null, null, false, (received) => {
|
|
614
|
+
status = received;
|
|
615
|
+
});
|
|
616
|
+
await Promise.race([
|
|
617
|
+
new Promise((resolve, reject) => {
|
|
618
|
+
socket.on('close', () => resolve());
|
|
619
|
+
socket.on('error', reject);
|
|
620
|
+
}),
|
|
621
|
+
(0, sleep_1.sleep)(KILL_TIMEOUT_MS).then(() => Promise.reject(new Error('the kill did not answer'))),
|
|
622
|
+
]);
|
|
623
|
+
// what it answers does not matter, that it answered does
|
|
624
|
+
if (getExecExitCode(status) === undefined) {
|
|
625
|
+
throw new Error(status?.message ?? 'the kill did not end');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
//# sourceMappingURL=sandbox-runner.js.map
|