@agiledigital/pingone-aic-script-tester 0.1.0
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +274 -0
- package/dist/bin/fetch-jar.d.ts +2 -0
- package/dist/bin/fetch-jar.js +13 -0
- package/dist/bin/pull-profile.d.ts +2 -0
- package/dist/bin/pull-profile.js +40 -0
- package/dist/bin/show-log.d.ts +2 -0
- package/dist/bin/show-log.js +31 -0
- package/dist/classes/.complete +12 -0
- package/dist/classes/AmClassShutter.class +0 -0
- package/dist/classes/AmContextFactory$ObservedContext.class +0 -0
- package/dist/classes/AmContextFactory.class +0 -0
- package/dist/classes/AmScopes.class +0 -0
- package/dist/classes/HostOps.class +0 -0
- package/dist/classes/JsValues.class +0 -0
- package/dist/classes/Json$Parser.class +0 -0
- package/dist/classes/Json.class +0 -0
- package/dist/classes/LanguageVersions.class +0 -0
- package/dist/classes/Runner.class +0 -0
- package/dist/classes/ScriptContextScope.class +0 -0
- package/dist/src/aic/callbacks.d.ts +35 -0
- package/dist/src/aic/callbacks.js +130 -0
- package/dist/src/aic/conform.d.ts +90 -0
- package/dist/src/aic/conform.js +193 -0
- package/dist/src/aic/constants.d.ts +13 -0
- package/dist/src/aic/constants.js +13 -0
- package/dist/src/aic/diff.d.ts +25 -0
- package/dist/src/aic/diff.js +201 -0
- package/dist/src/aic/emit-journey.d.ts +64 -0
- package/dist/src/aic/emit-journey.js +254 -0
- package/dist/src/aic/emit-result.d.ts +10 -0
- package/dist/src/aic/emit-result.js +39 -0
- package/dist/src/aic/emit-session.d.ts +26 -0
- package/dist/src/aic/emit-session.js +132 -0
- package/dist/src/aic/emit-subject.d.ts +40 -0
- package/dist/src/aic/emit-subject.js +210 -0
- package/dist/src/aic/file-lease.d.ts +35 -0
- package/dist/src/aic/file-lease.js +543 -0
- package/dist/src/aic/http.d.ts +21 -0
- package/dist/src/aic/http.js +127 -0
- package/dist/src/aic/idm.d.ts +10 -0
- package/dist/src/aic/idm.js +112 -0
- package/dist/src/aic/index.d.ts +35 -0
- package/dist/src/aic/index.js +21 -0
- package/dist/src/aic/js.d.ts +8 -0
- package/dist/src/aic/js.js +11 -0
- package/dist/src/aic/lease-identity.d.ts +38 -0
- package/dist/src/aic/lease-identity.js +72 -0
- package/dist/src/aic/lease-lock.d.ts +45 -0
- package/dist/src/aic/lease-lock.js +173 -0
- package/dist/src/aic/managed.d.ts +28 -0
- package/dist/src/aic/managed.js +199 -0
- package/dist/src/aic/provider.d.ts +156 -0
- package/dist/src/aic/provider.js +416 -0
- package/dist/src/aic/record.d.ts +28 -0
- package/dist/src/aic/record.js +183 -0
- package/dist/src/aic/resource-snapshot.d.ts +12 -0
- package/dist/src/aic/resource-snapshot.js +116 -0
- package/dist/src/aic/run.d.ts +102 -0
- package/dist/src/aic/run.js +450 -0
- package/dist/src/aic/tenant.d.ts +107 -0
- package/dist/src/aic/tenant.js +307 -0
- package/dist/src/aic/trace.d.ts +25 -0
- package/dist/src/aic/trace.js +56 -0
- package/dist/src/aic/txid.d.ts +11 -0
- package/dist/src/aic/txid.js +25 -0
- package/dist/src/aic/unsupported.d.ts +15 -0
- package/dist/src/aic/unsupported.js +60 -0
- package/dist/src/bindings/allowlist.d.ts +42 -0
- package/dist/src/bindings/allowlist.js +62 -0
- package/dist/src/bindings/harvest.d.ts +7 -0
- package/dist/src/bindings/harvest.js +137 -0
- package/dist/src/bindings/index.d.ts +4 -0
- package/dist/src/bindings/index.js +3 -0
- package/dist/src/bindings/preamble.d.ts +23 -0
- package/dist/src/bindings/preamble.js +36 -0
- package/dist/src/bindings/run.d.ts +33 -0
- package/dist/src/bindings/run.js +55 -0
- package/dist/src/case/equal.d.ts +2 -0
- package/dist/src/case/equal.js +40 -0
- package/dist/src/case/index.d.ts +5 -0
- package/dist/src/case/index.js +4 -0
- package/dist/src/case/portable.d.ts +10 -0
- package/dist/src/case/portable.js +28 -0
- package/dist/src/case/session.d.ts +34 -0
- package/dist/src/case/session.js +66 -0
- package/dist/src/case/state.d.ts +4 -0
- package/dist/src/case/state.js +47 -0
- package/dist/src/case/types.d.ts +266 -0
- package/dist/src/case/types.js +106 -0
- package/dist/src/case/util.d.ts +7 -0
- package/dist/src/case/util.js +115 -0
- package/dist/src/case/validate.d.ts +8 -0
- package/dist/src/case/validate.js +463 -0
- package/dist/src/case/verdict.d.ts +6 -0
- package/dist/src/case/verdict.js +810 -0
- package/dist/src/diagnostics.d.ts +7 -0
- package/dist/src/diagnostics.js +5 -0
- package/dist/src/emit-dts.d.ts +2 -0
- package/dist/src/emit-dts.js +82 -0
- package/dist/src/emit-js.d.ts +3 -0
- package/dist/src/emit-js.js +149 -0
- package/dist/src/harness/failures.d.ts +26 -0
- package/dist/src/harness/failures.js +113 -0
- package/dist/src/harness/idm.d.ts +12 -0
- package/dist/src/harness/idm.js +44 -0
- package/dist/src/harness/index.d.ts +11 -0
- package/dist/src/harness/index.js +6 -0
- package/dist/src/harness/lease.d.ts +136 -0
- package/dist/src/harness/lease.js +286 -0
- package/dist/src/harness/residue.d.ts +23 -0
- package/dist/src/harness/residue.js +73 -0
- package/dist/src/harness/show-log.d.ts +43 -0
- package/dist/src/harness/show-log.js +253 -0
- package/dist/src/harness/spec.d.ts +67 -0
- package/dist/src/harness/spec.js +189 -0
- package/dist/src/harness/step.d.ts +80 -0
- package/dist/src/harness/step.js +97 -0
- package/dist/src/harness/types.d.ts +100 -0
- package/dist/src/harness/types.js +1 -0
- package/dist/src/harness/vitest.d.ts +52 -0
- package/dist/src/harness/vitest.js +155 -0
- package/dist/src/idents.d.ts +4 -0
- package/dist/src/idents.js +42 -0
- package/dist/src/jvm.d.ts +124 -0
- package/dist/src/jvm.js +432 -0
- package/dist/src/load.d.ts +2 -0
- package/dist/src/load.js +104 -0
- package/dist/src/paths.d.ts +36 -0
- package/dist/src/paths.js +50 -0
- package/dist/src/profile/index.d.ts +5 -0
- package/dist/src/profile/index.js +3 -0
- package/dist/src/profile/normalise.d.ts +16 -0
- package/dist/src/profile/normalise.js +106 -0
- package/dist/src/profile/pull.d.ts +22 -0
- package/dist/src/profile/pull.js +36 -0
- package/dist/src/profile/seed.d.ts +11 -0
- package/dist/src/profile/seed.js +28 -0
- package/dist/src/profile/store.d.ts +13 -0
- package/dist/src/profile/store.js +60 -0
- package/dist/src/profile/types.d.ts +45 -0
- package/dist/src/profile/types.js +1 -0
- package/dist/src/profile/validate.d.ts +38 -0
- package/dist/src/profile/validate.js +129 -0
- package/dist/src/project.d.ts +37 -0
- package/dist/src/project.js +93 -0
- package/dist/src/protocol.d.ts +62 -0
- package/dist/src/protocol.js +68 -0
- package/dist/src/provider-module.d.ts +11 -0
- package/dist/src/provider-module.js +46 -0
- package/dist/src/runner.d.ts +39 -0
- package/dist/src/runner.js +337 -0
- package/dist/src/schema.d.ts +48 -0
- package/dist/src/schema.js +93 -0
- package/generated/scripted-decision-mocks.cjs +722 -0
- package/generated/scripted-decision-mocks.d.ts +626 -0
- package/generated/scripted-decision-next.json +3640 -0
- package/java/AmClassShutter.java +98 -0
- package/java/AmContextFactory.java +69 -0
- package/java/AmScopes.java +49 -0
- package/java/HostOps.java +180 -0
- package/java/JsValues.java +39 -0
- package/java/Json.java +441 -0
- package/java/LanguageVersions.java +55 -0
- package/java/Runner.java +289 -0
- package/java/ScriptContextScope.java +147 -0
- package/package.json +88 -0
- package/src/bindings/rhino/runtime.cjs +3021 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** JSON values the runner protocol can carry (job globals and completion values). */
|
|
2
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Machine-readable job outcomes. These are different bugs; the harness must
|
|
7
|
+
* not collapse them into one string.
|
|
8
|
+
*
|
|
9
|
+
* - `ok` — compiled and ran
|
|
10
|
+
* - `compile_error` — failed to compile
|
|
11
|
+
* - `runtime_error` — threw at runtime
|
|
12
|
+
* - `timeout` — instruction observer fired (`Error("Interrupt.")`)
|
|
13
|
+
* - `protocol_error` — the job itself was malformed
|
|
14
|
+
*/
|
|
15
|
+
export type JobOutcome = "ok" | "compile_error" | "runtime_error" | "timeout" | "protocol_error";
|
|
16
|
+
export interface JobRequest {
|
|
17
|
+
/** Correlates the response. Generated if omitted. */
|
|
18
|
+
id?: string;
|
|
19
|
+
source: string;
|
|
20
|
+
/** Author's file path; becomes Rhino's source name (not `<eval>`). */
|
|
21
|
+
sourceName?: string;
|
|
22
|
+
/** Rhino language version. Default 0 (`VERSION_DEFAULT`). */
|
|
23
|
+
languageVersion?: number | string;
|
|
24
|
+
/**
|
|
25
|
+
* Per-job timeout in milliseconds. `0` means no timeout (AM's default).
|
|
26
|
+
* Omitted uses the runner's harness default (10s) so a runaway cannot hang
|
|
27
|
+
* the process.
|
|
28
|
+
*/
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** JSON values placed in ENGINE_SCOPE Bindings before eval. */
|
|
31
|
+
globals?: {
|
|
32
|
+
[key: string]: JsonValue;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Java class allow-list for this job, as a script context's `allowLists`
|
|
36
|
+
* entries (exact names, or a trailing `*` meaning prefix). Supplying one
|
|
37
|
+
* installs AM's class shutter; omitting it leaves every Java name resolvable,
|
|
38
|
+
* which is the pre-shutter behaviour.
|
|
39
|
+
*/
|
|
40
|
+
classAllowList?: string[];
|
|
41
|
+
/** Evaluated first, so author line numbers on `source` stay intact. */
|
|
42
|
+
preamble?: string;
|
|
43
|
+
preambleName?: string;
|
|
44
|
+
/** Return this global after evaluation instead of Rhino's completion value. */
|
|
45
|
+
resultGlobal?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface JobError {
|
|
48
|
+
class: string;
|
|
49
|
+
message: string | null;
|
|
50
|
+
sourceName: string | null;
|
|
51
|
+
line: number;
|
|
52
|
+
column: number;
|
|
53
|
+
lineSource: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface JobResponse {
|
|
56
|
+
id: string;
|
|
57
|
+
outcome: JobOutcome;
|
|
58
|
+
valueKind: string;
|
|
59
|
+
value: JsonValue;
|
|
60
|
+
error: JobError | null;
|
|
61
|
+
}
|
|
62
|
+
export declare function parseJobResponse(raw: unknown, line: string): JobResponse;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const OUTCOMES = new Set([
|
|
2
|
+
"ok",
|
|
3
|
+
"compile_error",
|
|
4
|
+
"runtime_error",
|
|
5
|
+
"timeout",
|
|
6
|
+
"protocol_error",
|
|
7
|
+
]);
|
|
8
|
+
export function parseJobResponse(raw, line) {
|
|
9
|
+
if (!isRecord(raw)) {
|
|
10
|
+
throw new Error(`rhino-local: runner response is not an object: ${line}`);
|
|
11
|
+
}
|
|
12
|
+
if (typeof raw.id !== "string" || raw.id.length === 0) {
|
|
13
|
+
throw new Error(`rhino-local: runner response has no id: ${line}`);
|
|
14
|
+
}
|
|
15
|
+
if (typeof raw.outcome !== "string" || !OUTCOMES.has(raw.outcome)) {
|
|
16
|
+
throw new Error(`rhino-local: runner response has unknown outcome: ${line}`);
|
|
17
|
+
}
|
|
18
|
+
if (typeof raw.valueKind !== "string") {
|
|
19
|
+
throw new Error(`rhino-local: runner response is missing valueKind: ${line}`);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
id: raw.id,
|
|
23
|
+
outcome: raw.outcome,
|
|
24
|
+
valueKind: raw.valueKind,
|
|
25
|
+
value: raw.value,
|
|
26
|
+
error: parseJobError(raw.error),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function parseJobError(raw) {
|
|
30
|
+
if (raw === null || raw === undefined) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(raw)) {
|
|
34
|
+
throw new Error("rhino-local: runner error is not an object");
|
|
35
|
+
}
|
|
36
|
+
if (typeof raw.class !== "string") {
|
|
37
|
+
throw new Error("rhino-local: runner error is missing class");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
class: raw.class,
|
|
41
|
+
message: optionalString(raw.message),
|
|
42
|
+
sourceName: optionalString(raw.sourceName),
|
|
43
|
+
line: optionalNumber(raw.line, -1),
|
|
44
|
+
column: optionalNumber(raw.column, -1),
|
|
45
|
+
lineSource: optionalString(raw.lineSource),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function optionalString(raw) {
|
|
49
|
+
if (raw === null || raw === undefined) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (typeof raw !== "string") {
|
|
53
|
+
throw new Error("rhino-local: expected string or null");
|
|
54
|
+
}
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
function optionalNumber(raw, fallback) {
|
|
58
|
+
if (raw === null || raw === undefined) {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
62
|
+
throw new Error("rhino-local: expected finite number");
|
|
63
|
+
}
|
|
64
|
+
return raw;
|
|
65
|
+
}
|
|
66
|
+
function isRecord(value) {
|
|
67
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TenantProvider } from "./aic/provider.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Load a caller's tenant-provider module for a bin (`--provider-module`).
|
|
4
|
+
*
|
|
5
|
+
* A token callback lives in the consumer's code, and a bin is a separate
|
|
6
|
+
* process from the Vitest run that registered it, so the bins take the same
|
|
7
|
+
* module the consumer's `setupFiles` uses. Either shape works: a module whose
|
|
8
|
+
* default export is a `TenantProvider`, or one that calls
|
|
9
|
+
* `setTenantProvider()` when imported.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadProviderModule(path: string, cwd?: string): Promise<TenantProvider>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { configuredTenantProvider, setTenantProvider, TenantProviderError, } from "./aic/provider.js";
|
|
12
|
+
/**
|
|
13
|
+
* Load a caller's tenant-provider module for a bin (`--provider-module`).
|
|
14
|
+
*
|
|
15
|
+
* A token callback lives in the consumer's code, and a bin is a separate
|
|
16
|
+
* process from the Vitest run that registered it, so the bins take the same
|
|
17
|
+
* module the consumer's `setupFiles` uses. Either shape works: a module whose
|
|
18
|
+
* default export is a `TenantProvider`, or one that calls
|
|
19
|
+
* `setTenantProvider()` when imported.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadProviderModule(path, cwd = process.cwd()) {
|
|
22
|
+
const url = pathToFileURL(resolve(cwd, path)).href;
|
|
23
|
+
// Clear first, so the only provider that can be registered afterwards is
|
|
24
|
+
// one this module supplied: a module that supplies none must not leave an
|
|
25
|
+
// earlier registration — another tenant — in force.
|
|
26
|
+
setTenantProvider(undefined);
|
|
27
|
+
const loaded = (await import(__rewriteRelativeImportExtension(url)));
|
|
28
|
+
if (isTenantProvider(loaded.default)) {
|
|
29
|
+
setTenantProvider(loaded.default);
|
|
30
|
+
return loaded.default;
|
|
31
|
+
}
|
|
32
|
+
// An empty environment leaves only the registry to answer.
|
|
33
|
+
const registered = configuredTenantProvider({});
|
|
34
|
+
if (registered === undefined) {
|
|
35
|
+
throw new TenantProviderError(`${path} neither default-exports a TenantProvider nor calls setTenantProvider() ` +
|
|
36
|
+
"(a module that was already imported does not run again, so it cannot register twice)");
|
|
37
|
+
}
|
|
38
|
+
return registered;
|
|
39
|
+
}
|
|
40
|
+
function isTenantProvider(value) {
|
|
41
|
+
if (typeof value !== "object" || value === null) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const candidate = value;
|
|
45
|
+
return typeof candidate.describe === "function" && typeof candidate.getToken === "function";
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type JvmLane, type RunnerEnvironment } from "./jvm.ts";
|
|
2
|
+
import { type JobRequest, type JobResponse } from "./protocol.ts";
|
|
3
|
+
export declare class RhinoRunnerExitError extends Error {
|
|
4
|
+
readonly exitCode: number | null;
|
|
5
|
+
readonly signal: NodeJS.Signals | null;
|
|
6
|
+
readonly stderr: string;
|
|
7
|
+
constructor(exitCode: number | null, signal: NodeJS.Signals | null, stderr: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The host JVM and the AM image's JVM answered one job differently. This is
|
|
11
|
+
* the whole reason the `both` lane exists, so it fails the test rather than
|
|
12
|
+
* picking a winner.
|
|
13
|
+
*/
|
|
14
|
+
export declare class LaneDivergenceError extends Error {
|
|
15
|
+
readonly job: string;
|
|
16
|
+
readonly host: JobResponse;
|
|
17
|
+
readonly container: JobResponse;
|
|
18
|
+
constructor(job: string, host: JobResponse, container: JobResponse);
|
|
19
|
+
}
|
|
20
|
+
export interface SpawnRunnerOptions {
|
|
21
|
+
/** Default: `AIC_SCRIPT_TESTER_JVM`, else `host`. */
|
|
22
|
+
lane?: JvmLane;
|
|
23
|
+
/** Jar and compiled-class cache. Default: `AIC_SCRIPT_TESTER_CACHE`, else `~/.cache/aic-script-tester`. */
|
|
24
|
+
cache?: string;
|
|
25
|
+
spawnTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class RhinoRunner {
|
|
28
|
+
#private;
|
|
29
|
+
readonly lane: JvmLane;
|
|
30
|
+
private constructor();
|
|
31
|
+
static spawn(options?: SpawnRunnerOptions): Promise<RhinoRunner>;
|
|
32
|
+
/** What the (primary) JVM reported at startup. */
|
|
33
|
+
get environment(): RunnerEnvironment;
|
|
34
|
+
eval(job: JobRequest): Promise<JobResponse>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
/** Forcibly stop every JVM. Every pending eval rejects. Used by crash tests. */
|
|
37
|
+
kill(): Promise<void>;
|
|
38
|
+
get stderr(): string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isDeepStrictEqual, promisify } from "node:util";
|
|
4
|
+
import { cacheDir, checkEnvironment, containerLaunch, ensureRhinoJar, ensureRunnerClasses, hostLaunch, laneFromEnv, parseEnvironment, } from "./jvm.js";
|
|
5
|
+
import { parseJobResponse } from "./protocol.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const READY = "rhino-local-runner ready";
|
|
8
|
+
export class RhinoRunnerExitError extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
signal;
|
|
11
|
+
stderr;
|
|
12
|
+
constructor(exitCode, signal, stderr) {
|
|
13
|
+
super(`rhino-local: JVM runner exited (code=${exitCode}, signal=${signal})` +
|
|
14
|
+
(stderr.trim() ? `: ${stderr.trim()}` : ""));
|
|
15
|
+
this.name = "RhinoRunnerExitError";
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
this.signal = signal;
|
|
18
|
+
this.stderr = stderr;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The host JVM and the AM image's JVM answered one job differently. This is
|
|
23
|
+
* the whole reason the `both` lane exists, so it fails the test rather than
|
|
24
|
+
* picking a winner.
|
|
25
|
+
*/
|
|
26
|
+
export class LaneDivergenceError extends Error {
|
|
27
|
+
job;
|
|
28
|
+
host;
|
|
29
|
+
container;
|
|
30
|
+
constructor(job, host, container) {
|
|
31
|
+
super(`rhino-local: host and container JVMs disagree on ${job}\n` +
|
|
32
|
+
` host: ${JSON.stringify(withoutId(host))}\n` +
|
|
33
|
+
` container: ${JSON.stringify(withoutId(container))}`);
|
|
34
|
+
this.name = "LaneDivergenceError";
|
|
35
|
+
this.job = job;
|
|
36
|
+
this.host = host;
|
|
37
|
+
this.container = container;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function withoutId(response) {
|
|
41
|
+
const { id: _id, ...rest } = response;
|
|
42
|
+
return rest;
|
|
43
|
+
}
|
|
44
|
+
/** One JVM speaking the line protocol. {@link RhinoRunner} holds one or two. */
|
|
45
|
+
class JvmProcess {
|
|
46
|
+
lane;
|
|
47
|
+
containerName;
|
|
48
|
+
#child;
|
|
49
|
+
#pending = new Map();
|
|
50
|
+
#stdoutBuf = "";
|
|
51
|
+
#stderrBuf = "";
|
|
52
|
+
#closed = false;
|
|
53
|
+
#exit = null;
|
|
54
|
+
#exitWaiters = [];
|
|
55
|
+
environment;
|
|
56
|
+
constructor(lane, launch) {
|
|
57
|
+
this.lane = lane;
|
|
58
|
+
this.containerName = launch.containerName;
|
|
59
|
+
const child = spawn(launch.command, launch.args, {
|
|
60
|
+
env: launch.env,
|
|
61
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
62
|
+
});
|
|
63
|
+
this.#child = child;
|
|
64
|
+
child.stdout.setEncoding("utf8");
|
|
65
|
+
child.stderr.setEncoding("utf8");
|
|
66
|
+
child.stdout.on("data", (chunk) => this.#onStdout(chunk));
|
|
67
|
+
child.stderr.on("data", (chunk) => {
|
|
68
|
+
this.#stderrBuf += chunk;
|
|
69
|
+
});
|
|
70
|
+
child.stdin.on("error", () => {
|
|
71
|
+
// EPIPE after a crash is reported via the exit handler.
|
|
72
|
+
});
|
|
73
|
+
child.on("error", (error) => {
|
|
74
|
+
this.#stderrBuf += `\n${error.message}`;
|
|
75
|
+
});
|
|
76
|
+
child.on("exit", (code, signal) => {
|
|
77
|
+
this.#exit = { code, signal };
|
|
78
|
+
const error = new RhinoRunnerExitError(code, signal, this.#stderrBuf);
|
|
79
|
+
for (const [id, pending] of this.#pending) {
|
|
80
|
+
this.#pending.delete(id);
|
|
81
|
+
pending.reject(error);
|
|
82
|
+
}
|
|
83
|
+
for (const waiter of this.#exitWaiters) {
|
|
84
|
+
waiter();
|
|
85
|
+
}
|
|
86
|
+
this.#exitWaiters = [];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
get stderr() {
|
|
90
|
+
return this.#stderrBuf;
|
|
91
|
+
}
|
|
92
|
+
async waitUntilReady(timeoutMs) {
|
|
93
|
+
const started = Date.now();
|
|
94
|
+
for (;;) {
|
|
95
|
+
const at = this.#stderrBuf.indexOf(READY);
|
|
96
|
+
const end = at === -1 ? -1 : this.#stderrBuf.indexOf("\n", at);
|
|
97
|
+
if (end !== -1) {
|
|
98
|
+
const env = parseEnvironment(this.#stderrBuf.slice(at, end));
|
|
99
|
+
checkEnvironment(env, this.lane);
|
|
100
|
+
this.environment = env;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (this.#exit) {
|
|
104
|
+
throw new RhinoRunnerExitError(this.#exit.code, this.#exit.signal, this.#stderrBuf);
|
|
105
|
+
}
|
|
106
|
+
if (Date.now() - started > timeoutMs) {
|
|
107
|
+
await this.kill();
|
|
108
|
+
throw new Error(`rhino-local: ${this.lane} runner did not become ready within ${timeoutMs}ms: ${this.#stderrBuf}`);
|
|
109
|
+
}
|
|
110
|
+
await sleep(25);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
send(id, payload) {
|
|
114
|
+
if (this.#closed || this.#exit) {
|
|
115
|
+
return Promise.reject(this.#exit
|
|
116
|
+
? new RhinoRunnerExitError(this.#exit.code, this.#exit.signal, this.#stderrBuf)
|
|
117
|
+
: new Error("rhino-local: runner is closed"));
|
|
118
|
+
}
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
this.#pending.set(id, { resolve, reject });
|
|
121
|
+
const ok = this.#child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
122
|
+
if (!ok) {
|
|
123
|
+
this.#child.stdin.once("error", reject);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async close() {
|
|
128
|
+
if (this.#exit) {
|
|
129
|
+
await this.#removeContainer();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.#closed) {
|
|
133
|
+
await this.#waitForExit();
|
|
134
|
+
await this.#removeContainer();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.#closed = true;
|
|
138
|
+
try {
|
|
139
|
+
this.#child.stdin.end();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// already closed
|
|
143
|
+
}
|
|
144
|
+
const exited = await Promise.race([
|
|
145
|
+
this.#waitForExit().then(() => true),
|
|
146
|
+
sleep(5_000).then(() => false),
|
|
147
|
+
]);
|
|
148
|
+
if (!exited) {
|
|
149
|
+
this.#child.kill("SIGTERM");
|
|
150
|
+
const exitedAfterTerm = await Promise.race([
|
|
151
|
+
this.#waitForExit().then(() => true),
|
|
152
|
+
sleep(2_000).then(() => false),
|
|
153
|
+
]);
|
|
154
|
+
if (!exitedAfterTerm) {
|
|
155
|
+
this.#child.kill("SIGKILL");
|
|
156
|
+
await this.#waitForExit();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const [id, pending] of this.#pending) {
|
|
160
|
+
this.#pending.delete(id);
|
|
161
|
+
pending.reject(new Error("rhino-local: runner closed with job still pending"));
|
|
162
|
+
}
|
|
163
|
+
await this.#removeContainer();
|
|
164
|
+
}
|
|
165
|
+
async kill() {
|
|
166
|
+
this.#closed = true;
|
|
167
|
+
this.#child.kill("SIGKILL");
|
|
168
|
+
await Promise.race([this.#waitForExit(), sleep(2_000)]);
|
|
169
|
+
await this.#removeContainer();
|
|
170
|
+
}
|
|
171
|
+
#onStdout(chunk) {
|
|
172
|
+
this.#stdoutBuf += chunk;
|
|
173
|
+
let nl = this.#stdoutBuf.indexOf("\n");
|
|
174
|
+
while (nl !== -1) {
|
|
175
|
+
const line = this.#stdoutBuf.slice(0, nl);
|
|
176
|
+
this.#stdoutBuf = this.#stdoutBuf.slice(nl + 1);
|
|
177
|
+
if (line.length > 0) {
|
|
178
|
+
this.#dispatch(line);
|
|
179
|
+
}
|
|
180
|
+
nl = this.#stdoutBuf.indexOf("\n");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
#dispatch(line) {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(line);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const first = this.#pending.entries().next().value;
|
|
190
|
+
if (first) {
|
|
191
|
+
this.#pending.delete(first[0]);
|
|
192
|
+
first[1].reject(new Error(`rhino-local: unparseable runner stdout: ${line} (${String(error)})`));
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let response;
|
|
197
|
+
try {
|
|
198
|
+
response = parseJobResponse(parsed, line);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const first = this.#pending.entries().next().value;
|
|
202
|
+
if (first) {
|
|
203
|
+
this.#pending.delete(first[0]);
|
|
204
|
+
first[1].reject(error instanceof Error ? error : new Error(String(error)));
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const pending = this.#pending.get(response.id);
|
|
209
|
+
if (!pending) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.#pending.delete(response.id);
|
|
213
|
+
pending.resolve(response);
|
|
214
|
+
}
|
|
215
|
+
#waitForExit() {
|
|
216
|
+
if (this.#exit) {
|
|
217
|
+
return Promise.resolve();
|
|
218
|
+
}
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
this.#exitWaiters.push(resolve);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async #removeContainer() {
|
|
224
|
+
if (this.containerName === undefined) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
await execFileAsync("docker", ["rm", "-f", this.containerName], { timeout: 10_000 });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// already removed via --rm, or never created
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export class RhinoRunner {
|
|
236
|
+
lane;
|
|
237
|
+
#primary;
|
|
238
|
+
#shadow;
|
|
239
|
+
constructor(lane, primary, shadow) {
|
|
240
|
+
this.lane = lane;
|
|
241
|
+
this.#primary = primary;
|
|
242
|
+
this.#shadow = shadow;
|
|
243
|
+
}
|
|
244
|
+
static async spawn(options = {}) {
|
|
245
|
+
const lane = options.lane ?? laneFromEnv();
|
|
246
|
+
const cache = options.cache ?? cacheDir();
|
|
247
|
+
const jar = await ensureRhinoJar(cache);
|
|
248
|
+
const classes = await ensureRunnerClasses(jar, { cache });
|
|
249
|
+
const spawnTimeoutMs = options.spawnTimeoutMs ?? 60_000;
|
|
250
|
+
const processes = [];
|
|
251
|
+
try {
|
|
252
|
+
if (lane === "host" || lane === "both") {
|
|
253
|
+
processes.push(new JvmProcess("host", hostLaunch(classes, jar)));
|
|
254
|
+
}
|
|
255
|
+
if (lane === "container" || lane === "both") {
|
|
256
|
+
processes.push(new JvmProcess("container", containerLaunch(classes, jar)));
|
|
257
|
+
}
|
|
258
|
+
await Promise.all(processes.map((p) => p.waitUntilReady(spawnTimeoutMs)));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
await Promise.all(processes.map((p) => p.kill()));
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
const [primary, shadow] = processes;
|
|
265
|
+
if (primary === undefined) {
|
|
266
|
+
throw new Error("rhino-local: no runner lane selected");
|
|
267
|
+
}
|
|
268
|
+
return new RhinoRunner(lane, primary, shadow);
|
|
269
|
+
}
|
|
270
|
+
/** What the (primary) JVM reported at startup. */
|
|
271
|
+
get environment() {
|
|
272
|
+
const env = this.#primary.environment;
|
|
273
|
+
if (env === undefined) {
|
|
274
|
+
throw new Error("rhino-local: runner environment read before ready");
|
|
275
|
+
}
|
|
276
|
+
return env;
|
|
277
|
+
}
|
|
278
|
+
async eval(job) {
|
|
279
|
+
const id = job.id ?? randomUUID();
|
|
280
|
+
const payload = { id, source: job.source };
|
|
281
|
+
if (job.sourceName !== undefined) {
|
|
282
|
+
payload.sourceName = job.sourceName;
|
|
283
|
+
}
|
|
284
|
+
if (job.languageVersion !== undefined) {
|
|
285
|
+
payload.languageVersion = job.languageVersion;
|
|
286
|
+
}
|
|
287
|
+
if (job.timeoutMs !== undefined) {
|
|
288
|
+
payload.timeoutMs = job.timeoutMs;
|
|
289
|
+
}
|
|
290
|
+
if (job.globals !== undefined) {
|
|
291
|
+
payload.globals = job.globals;
|
|
292
|
+
}
|
|
293
|
+
if (job.preamble !== undefined) {
|
|
294
|
+
payload.preamble = job.preamble;
|
|
295
|
+
}
|
|
296
|
+
if (job.preambleName !== undefined) {
|
|
297
|
+
payload.preambleName = job.preambleName;
|
|
298
|
+
}
|
|
299
|
+
if (job.classAllowList !== undefined) {
|
|
300
|
+
payload.classAllowList = job.classAllowList;
|
|
301
|
+
}
|
|
302
|
+
if (job.resultGlobal !== undefined) {
|
|
303
|
+
payload.resultGlobal = job.resultGlobal;
|
|
304
|
+
}
|
|
305
|
+
if (this.#shadow === undefined) {
|
|
306
|
+
return this.#primary.send(id, payload);
|
|
307
|
+
}
|
|
308
|
+
const [host, container] = await Promise.all([
|
|
309
|
+
this.#primary.send(id, payload),
|
|
310
|
+
this.#shadow.send(id, payload),
|
|
311
|
+
]);
|
|
312
|
+
if (!isDeepStrictEqual(withoutId(host), withoutId(container))) {
|
|
313
|
+
throw new LaneDivergenceError(job.sourceName ?? id, host, container);
|
|
314
|
+
}
|
|
315
|
+
return host;
|
|
316
|
+
}
|
|
317
|
+
async close() {
|
|
318
|
+
await Promise.all(this.#processes().map((p) => p.close()));
|
|
319
|
+
}
|
|
320
|
+
/** Forcibly stop every JVM. Every pending eval rejects. Used by crash tests. */
|
|
321
|
+
async kill() {
|
|
322
|
+
await Promise.all(this.#processes().map((p) => p.kill()));
|
|
323
|
+
}
|
|
324
|
+
get stderr() {
|
|
325
|
+
return this.#processes()
|
|
326
|
+
.map((p) => p.stderr)
|
|
327
|
+
.join("");
|
|
328
|
+
}
|
|
329
|
+
#processes() {
|
|
330
|
+
return this.#shadow === undefined ? [this.#primary] : [this.#primary, this.#shadow];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function sleep(ms) {
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
setTimeout(resolve, ms);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Contexts-endpoint binding metadata (`docs/api/bindings/*.json`). */
|
|
2
|
+
export declare const JS_TYPES: readonly ["string", "number", "boolean", "object", "array", "unknown", "void"];
|
|
3
|
+
export type JsType = (typeof JS_TYPES)[number];
|
|
4
|
+
export interface Parameter {
|
|
5
|
+
name: string;
|
|
6
|
+
javaScriptType: JsType;
|
|
7
|
+
}
|
|
8
|
+
export interface MethodElement {
|
|
9
|
+
elementType: "method";
|
|
10
|
+
name: string;
|
|
11
|
+
parameters: Parameter[];
|
|
12
|
+
returnType: JsType;
|
|
13
|
+
}
|
|
14
|
+
export interface FieldElement {
|
|
15
|
+
elementType: "field";
|
|
16
|
+
name: string;
|
|
17
|
+
javaScriptType: JsType;
|
|
18
|
+
elements: Element[];
|
|
19
|
+
}
|
|
20
|
+
export type Element = MethodElement | FieldElement;
|
|
21
|
+
export interface Binding {
|
|
22
|
+
name: string;
|
|
23
|
+
javaScriptType: JsType;
|
|
24
|
+
elements: Element[];
|
|
25
|
+
}
|
|
26
|
+
export interface ContextsDocument {
|
|
27
|
+
_id: string;
|
|
28
|
+
bindings: Binding[];
|
|
29
|
+
}
|
|
30
|
+
export type BindingKind = "scalar" | "opaque" | "object";
|
|
31
|
+
export declare function classifyBinding(binding: Binding): BindingKind;
|
|
32
|
+
export interface MethodMember {
|
|
33
|
+
kind: "method";
|
|
34
|
+
name: string;
|
|
35
|
+
overloads: MethodElement[];
|
|
36
|
+
}
|
|
37
|
+
export interface FieldMember {
|
|
38
|
+
kind: "field";
|
|
39
|
+
name: string;
|
|
40
|
+
javaScriptType: JsType;
|
|
41
|
+
elements: Element[];
|
|
42
|
+
}
|
|
43
|
+
export type Member = MethodMember | FieldMember;
|
|
44
|
+
/** Group overloads of the same name; preserve first-seen member order. */
|
|
45
|
+
export declare function membersOf(elements: Element[]): Member[];
|
|
46
|
+
export declare function uniqueOverloads(overloads: MethodElement[]): MethodElement[];
|
|
47
|
+
export declare function overloadKey(overload: MethodElement): string;
|
|
48
|
+
export declare function overloadLabel(overload: MethodElement): string;
|