rightmodeler 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.
@@ -0,0 +1,172 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ createWriteStream,
4
+ mkdirSync,
5
+ readFileSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { finished } from "node:stream/promises";
10
+
11
+ const APP_ROOT = "/rightmodeler/app";
12
+
13
+ const configPath = process.argv[2];
14
+ if (configPath === undefined) throw new Error("Supervisor config is required");
15
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
16
+ const scratch = process.env.RM_SCRATCH;
17
+ if (scratch === undefined || scratch.length === 0) {
18
+ throw new Error("RM_SCRATCH is required");
19
+ }
20
+
21
+ const driverDirectory = join(scratch, "driver");
22
+ const workloadDirectory = join(scratch, "workload");
23
+ const proxyDirectory = join(scratch, "proxy");
24
+ mkdirSync(driverDirectory, { recursive: true });
25
+ mkdirSync(workloadDirectory, { recursive: true });
26
+ mkdirSync(proxyDirectory, { recursive: true });
27
+ const statusPath = join(driverDirectory, "status.json");
28
+ const heartbeat = setInterval(
29
+ () => writeFileSync(join(scratch, ".heartbeat"), new Date().toISOString()),
30
+ 500,
31
+ );
32
+ heartbeat.unref();
33
+
34
+ function start(command, stdoutPath, stderrPath, flags = "w") {
35
+ const stdout = createWriteStream(stdoutPath, { flags });
36
+ const stderr = createWriteStream(stderrPath, { flags });
37
+ const child = spawn(command[0], command.slice(1), {
38
+ cwd: APP_ROOT,
39
+ stdio: ["ignore", "pipe", "pipe"],
40
+ });
41
+ child.stdout.pipe(stdout);
42
+ child.stderr.pipe(stderr);
43
+ const closed = new Promise((resolve, reject) => {
44
+ child.once("error", reject);
45
+ child.once("close", (code, signal) => resolve({ code, signal }));
46
+ });
47
+ return { child, closed, stdout, stderr };
48
+ }
49
+
50
+ async function waitForRun(running) {
51
+ let result;
52
+ try {
53
+ result = await running.closed;
54
+ } catch (error) {
55
+ running.stdout.destroy();
56
+ running.stderr.destroy();
57
+ throw error;
58
+ }
59
+ await Promise.all([finished(running.stdout), finished(running.stderr)]);
60
+ return result;
61
+ }
62
+
63
+ async function execute(command, label) {
64
+ return waitForRun(
65
+ start(
66
+ command,
67
+ join(workloadDirectory, `${label}-stdout.log`),
68
+ join(workloadDirectory, `${label}-stderr.log`),
69
+ "a",
70
+ ),
71
+ );
72
+ }
73
+
74
+ async function startProxy() {
75
+ const running = start(
76
+ [process.execPath, "/rightmodeler/runtime/proxy/proxy-runtime.mjs"],
77
+ join(proxyDirectory, "stdout.log"),
78
+ join(proxyDirectory, "stderr.log"),
79
+ "a",
80
+ );
81
+ let pending = "";
82
+ await new Promise((resolve, reject) => {
83
+ const timer = setTimeout(
84
+ () => reject(new Error("Proxy readiness timed out")),
85
+ 10_000,
86
+ );
87
+ running.child.once("error", (error) => {
88
+ clearTimeout(timer);
89
+ reject(error);
90
+ });
91
+ running.child.once("close", (code) => {
92
+ clearTimeout(timer);
93
+ reject(new Error(`Proxy exited before readiness: ${String(code)}`));
94
+ });
95
+ running.child.stdout.on("data", (chunk) => {
96
+ pending += chunk.toString("utf8");
97
+ let newline = pending.indexOf("\n");
98
+ while (newline !== -1) {
99
+ const line = pending.slice(0, newline);
100
+ pending = pending.slice(newline + 1);
101
+ try {
102
+ const event = JSON.parse(line);
103
+ if (event.event === "ready" && event.port === 8787) {
104
+ clearTimeout(timer);
105
+ resolve();
106
+ return;
107
+ }
108
+ } catch {
109
+ // Proxy diagnostics may precede its structured readiness line.
110
+ }
111
+ newline = pending.indexOf("\n");
112
+ }
113
+ });
114
+ });
115
+ return running;
116
+ }
117
+
118
+ async function stopProxy(proxy) {
119
+ if (proxy.child.exitCode === null && proxy.child.signalCode === null) {
120
+ proxy.child.kill("SIGTERM");
121
+ await Promise.race([
122
+ proxy.closed,
123
+ new Promise((resolve) => setTimeout(resolve, 1_000)),
124
+ ]);
125
+ if (proxy.child.exitCode === null && proxy.child.signalCode === null) {
126
+ proxy.child.kill("SIGKILL");
127
+ }
128
+ }
129
+ await proxy.closed;
130
+ await Promise.all([finished(proxy.stdout), finished(proxy.stderr)]);
131
+ }
132
+
133
+ let phase = "install";
134
+ let proxy;
135
+ try {
136
+ if (config.installCommand !== undefined) {
137
+ const installed = await execute(config.installCommand, "install");
138
+ if (installed.code !== 0) {
139
+ writeFileSync(statusPath, JSON.stringify({ phase, ...installed }));
140
+ process.exitCode = installed.code ?? 1;
141
+ }
142
+ }
143
+
144
+ if (process.exitCode === undefined) {
145
+ phase = "proxy";
146
+ proxy = await startProxy();
147
+ phase = "workload";
148
+ const workload = await waitForRun(
149
+ start(
150
+ config.command,
151
+ join(workloadDirectory, "stdout.jsonl"),
152
+ join(workloadDirectory, "stderr.log"),
153
+ ),
154
+ );
155
+ writeFileSync(statusPath, JSON.stringify({ phase, ...workload }));
156
+ await stopProxy(proxy);
157
+ proxy = undefined;
158
+ process.exitCode = workload.code ?? 1;
159
+ }
160
+ } catch (error) {
161
+ writeFileSync(
162
+ statusPath,
163
+ JSON.stringify({
164
+ phase,
165
+ error: error instanceof Error ? error.message : String(error),
166
+ }),
167
+ );
168
+ if (proxy !== undefined) await stopProxy(proxy);
169
+ process.exitCode = 1;
170
+ } finally {
171
+ clearInterval(heartbeat);
172
+ }
@@ -0,0 +1,10 @@
1
+ export const hopByHopHeaders = new Set([
2
+ "connection",
3
+ "keep-alive",
4
+ "proxy-authenticate",
5
+ "proxy-authorization",
6
+ "te",
7
+ "trailer",
8
+ "transfer-encoding",
9
+ "upgrade",
10
+ ]);