coderaft 0.0.25 → 0.0.26
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 +28 -0
- package/code.tar.zst +0 -0
- package/dist/_chunks/server.mjs +9 -1
- package/dist/index.d.mts +64 -1
- package/dist/index.mjs +170 -1
- package/package.json +3 -1
- package/worker.mjs +64 -0
package/README.md
CHANGED
|
@@ -135,6 +135,34 @@ interface CodeServerHandle {
|
|
|
135
135
|
}
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
### `spawnCodeServer(options)`
|
|
139
|
+
|
|
140
|
+
Runs the code-server in a forked Node.js child process — useful for isolating VS Code's singletons from the host or restarting the server without taking the host down. Accepts the same options as `startCodeServer`, plus a `spawn` sub-object (`env`, `execArgv`, `stdio`, `startupTimeout`).
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { spawnCodeServer } from "coderaft";
|
|
144
|
+
|
|
145
|
+
const instance = await spawnCodeServer({
|
|
146
|
+
port: 6063,
|
|
147
|
+
defaultFolder: "/path/to/workspace",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log(`Ready at ${instance.url}`);
|
|
151
|
+
|
|
152
|
+
// Notify on unexpected worker exits (not fired during close/reload):
|
|
153
|
+
instance.on("exit", (code, signal) => {
|
|
154
|
+
console.warn(`worker died code=${code} signal=${signal}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Restart the worker in place (reads fresh url/port/connectionToken after):
|
|
158
|
+
await instance.reload();
|
|
159
|
+
|
|
160
|
+
// Graceful shutdown — SIGTERM to the worker, then SIGKILL after 5s:
|
|
161
|
+
await instance.close();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Options cross an IPC boundary, so nested values (`vscode`, etc.) must be JSON-compatible.
|
|
165
|
+
|
|
138
166
|
## CLI Options
|
|
139
167
|
|
|
140
168
|
### Server
|
package/code.tar.zst
CHANGED
|
Binary file
|
package/dist/_chunks/server.mjs
CHANGED
|
@@ -317,11 +317,19 @@ async function startCodeServer(opts = {}) {
|
|
|
317
317
|
const address = server.address();
|
|
318
318
|
const actualPort = address && typeof address === "object" && "port" in address ? address.port : void 0;
|
|
319
319
|
const basePath = normalizeBaseURL(opts.baseURL ?? opts.vscode?.["server-base-path"]);
|
|
320
|
+
let url;
|
|
321
|
+
if (socketPath) url = `unix:${socketPath}`;
|
|
322
|
+
else {
|
|
323
|
+
const baseUrl = new URL(`http://localhost:${actualPort}${basePath}/`);
|
|
324
|
+
if (handler.connectionToken) baseUrl.searchParams.set("tkn", handler.connectionToken);
|
|
325
|
+
const rawUrl = opts.formatURL ? opts.formatURL(baseUrl) ?? baseUrl : baseUrl;
|
|
326
|
+
url = rawUrl instanceof URL ? rawUrl.toString() : rawUrl;
|
|
327
|
+
}
|
|
320
328
|
return {
|
|
321
329
|
server,
|
|
322
330
|
port: actualPort,
|
|
323
331
|
socketPath,
|
|
324
|
-
url
|
|
332
|
+
url,
|
|
325
333
|
connectionToken: handler.connectionToken,
|
|
326
334
|
async close() {
|
|
327
335
|
await handler.dispose();
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ChildProcess } from "node:child_process";
|
|
1
2
|
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
2
4
|
import { Duplex } from "node:stream";
|
|
3
5
|
/**
|
|
4
6
|
* Options for `createServer()` from VS Code server
|
|
@@ -81,6 +83,12 @@ interface VSCodeServerOptions {
|
|
|
81
83
|
interface CreateCodeServerOptions {
|
|
82
84
|
/** Workspace folder opened when no input is given in the URL. */
|
|
83
85
|
defaultFolder?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Customize the URL returned by the server. Called with the base URL after
|
|
88
|
+
* auth params are applied. Return a modified URL, a string, or `undefined`
|
|
89
|
+
* to keep the original.
|
|
90
|
+
*/
|
|
91
|
+
formatURL?: (url: URL) => string | URL | undefined;
|
|
84
92
|
/** Connection token (shared auth secret). Auto-generated if omitted. */
|
|
85
93
|
connectionToken?: string;
|
|
86
94
|
/** Host/interface to bind (used to infer local-only access for token default). */
|
|
@@ -120,4 +128,59 @@ interface CodeServerHandle {
|
|
|
120
128
|
}
|
|
121
129
|
declare function createCodeServer(opts?: CreateCodeServerOptions): Promise<CodeServerHandler>;
|
|
122
130
|
declare function startCodeServer(opts?: StartCodeServerOptions): Promise<CodeServerHandle>;
|
|
123
|
-
|
|
131
|
+
interface SpawnProcessOptions {
|
|
132
|
+
/** Extra environment variables merged into the worker's `process.env`. */
|
|
133
|
+
env?: NodeJS.ProcessEnv;
|
|
134
|
+
/** Node.js exec argv forwarded to the worker (e.g. `["--inspect"]`). */
|
|
135
|
+
execArgv?: string[];
|
|
136
|
+
/**
|
|
137
|
+
* stdio for the forked worker. Defaults to `"inherit"` for stdout/stderr and
|
|
138
|
+
* `"ignore"` for stdin. Pass `"pipe"` to capture output via `handle.proc`.
|
|
139
|
+
*/
|
|
140
|
+
stdio?: "inherit" | "pipe" | "ignore";
|
|
141
|
+
/**
|
|
142
|
+
* Max ms to wait for the worker to report `ready`. Defaults to 60000.
|
|
143
|
+
* On timeout, the worker is killed and the promise rejects.
|
|
144
|
+
*/
|
|
145
|
+
startupTimeout?: number;
|
|
146
|
+
}
|
|
147
|
+
interface SpawnCodeServerOptions extends StartCodeServerOptions {
|
|
148
|
+
/** Options for the forked worker process. */
|
|
149
|
+
spawn?: SpawnProcessOptions;
|
|
150
|
+
}
|
|
151
|
+
interface SpawnedCodeServer {
|
|
152
|
+
/**
|
|
153
|
+
* Emitted when the handle loses its worker: crash, explicit kill, or a
|
|
154
|
+
* `reload()` whose respawn failed (the old worker is already gone; the
|
|
155
|
+
* failure also rejects the `reload()` promise). Not emitted for `close()`
|
|
156
|
+
* or the successful half of `reload()`.
|
|
157
|
+
*/
|
|
158
|
+
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
159
|
+
/**
|
|
160
|
+
* Emitted for worker `error` events (spawn failure, IPC send failure) and
|
|
161
|
+
* for post-ready fatal errors forwarded by the worker (uncaughtException,
|
|
162
|
+
* unhandledRejection).
|
|
163
|
+
*/
|
|
164
|
+
on(event: "error", listener: (err: Error) => void): this;
|
|
165
|
+
on(event: string, listener: (...args: unknown[]) => void): this;
|
|
166
|
+
}
|
|
167
|
+
declare class SpawnedCodeServer extends EventEmitter {
|
|
168
|
+
#private;
|
|
169
|
+
/** The forked worker process (changes after `reload`). */
|
|
170
|
+
proc: ChildProcess;
|
|
171
|
+
url: string;
|
|
172
|
+
/** TCP port the server is bound to, or `undefined` when listening on a unix socket. */
|
|
173
|
+
port?: number;
|
|
174
|
+
/** Unix socket path the server is bound to, or `undefined` when listening on TCP. */
|
|
175
|
+
socketPath?: string;
|
|
176
|
+
connectionToken: string;
|
|
177
|
+
private constructor();
|
|
178
|
+
/** Spawn a new worker and resolve with a handle once it reports ready. */
|
|
179
|
+
static spawn(opts?: SpawnCodeServerOptions): Promise<SpawnedCodeServer>;
|
|
180
|
+
/** Terminate the worker process. Sends SIGTERM, then SIGKILL after 5s. Idempotent. */
|
|
181
|
+
close(): Promise<void>;
|
|
182
|
+
/** Kill the current worker and spawn a new one with the original options. */
|
|
183
|
+
reload(): Promise<void>;
|
|
184
|
+
}
|
|
185
|
+
declare function spawnCodeServer(opts?: SpawnCodeServerOptions): Promise<SpawnedCodeServer>;
|
|
186
|
+
export { type CodeServerHandle, type CodeServerHandler, type CreateCodeServerOptions, type SpawnCodeServerOptions, type SpawnProcessOptions, SpawnedCodeServer, type StartCodeServerOptions, createCodeServer, spawnCodeServer, startCodeServer };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,171 @@
|
|
|
1
1
|
import { r as startCodeServer, t as createCodeServer } from "./_chunks/server.mjs";
|
|
2
|
-
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { fork } from "node:child_process";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
var SpawnedCodeServer = class SpawnedCodeServer extends EventEmitter {
|
|
6
|
+
proc;
|
|
7
|
+
url;
|
|
8
|
+
port;
|
|
9
|
+
socketPath;
|
|
10
|
+
connectionToken;
|
|
11
|
+
#opts;
|
|
12
|
+
#detach;
|
|
13
|
+
#reloading;
|
|
14
|
+
#closed = false;
|
|
15
|
+
constructor(opts, state) {
|
|
16
|
+
super();
|
|
17
|
+
this.#opts = opts;
|
|
18
|
+
this.#adopt(state);
|
|
19
|
+
}
|
|
20
|
+
static async spawn(opts = {}) {
|
|
21
|
+
return new SpawnedCodeServer(opts, await spawnWorker(opts));
|
|
22
|
+
}
|
|
23
|
+
async close() {
|
|
24
|
+
if (this.#closed) return;
|
|
25
|
+
this.#closed = true;
|
|
26
|
+
if (this.#reloading) try {
|
|
27
|
+
await this.#reloading;
|
|
28
|
+
} catch {}
|
|
29
|
+
this.#detach();
|
|
30
|
+
await terminateChild(this.proc);
|
|
31
|
+
}
|
|
32
|
+
async reload() {
|
|
33
|
+
if (this.#closed) throw new Error("SpawnedCodeServer is closed");
|
|
34
|
+
if (this.#reloading) return this.#reloading;
|
|
35
|
+
this.#reloading = (async () => {
|
|
36
|
+
this.#detach();
|
|
37
|
+
await terminateChild(this.proc);
|
|
38
|
+
if (this.#closed) throw new Error("SpawnedCodeServer was closed during reload");
|
|
39
|
+
let next;
|
|
40
|
+
try {
|
|
41
|
+
next = await spawnWorker(this.#opts);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const dead = this.proc;
|
|
44
|
+
queueMicrotask(() => this.emit("exit", dead.exitCode, dead.signalCode));
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
if (this.#closed) {
|
|
48
|
+
await terminateChild(next.proc);
|
|
49
|
+
throw new Error("SpawnedCodeServer was closed during reload");
|
|
50
|
+
}
|
|
51
|
+
this.#adopt(next);
|
|
52
|
+
})();
|
|
53
|
+
try {
|
|
54
|
+
await this.#reloading;
|
|
55
|
+
} finally {
|
|
56
|
+
this.#reloading = void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
#adopt(state) {
|
|
60
|
+
this.proc = state.proc;
|
|
61
|
+
this.url = state.url;
|
|
62
|
+
this.port = state.port;
|
|
63
|
+
this.socketPath = state.socketPath;
|
|
64
|
+
this.connectionToken = state.connectionToken;
|
|
65
|
+
this.#detach = this.#attachListeners(state.proc);
|
|
66
|
+
}
|
|
67
|
+
#attachListeners(proc) {
|
|
68
|
+
const onExit = (code, signal) => this.emit("exit", code, signal);
|
|
69
|
+
const onError = (err) => this.emit("error", err);
|
|
70
|
+
const onMessage = (msg) => {
|
|
71
|
+
if (msg?.type === "error") this.emit("error", new Error(msg.message ?? "worker error"));
|
|
72
|
+
};
|
|
73
|
+
proc.on("exit", onExit);
|
|
74
|
+
proc.on("error", onError);
|
|
75
|
+
proc.on("message", onMessage);
|
|
76
|
+
return () => {
|
|
77
|
+
proc.off("exit", onExit);
|
|
78
|
+
proc.off("error", onError);
|
|
79
|
+
proc.off("message", onMessage);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function spawnCodeServer(opts = {}) {
|
|
84
|
+
return SpawnedCodeServer.spawn(opts);
|
|
85
|
+
}
|
|
86
|
+
async function spawnWorker(opts) {
|
|
87
|
+
const { spawn: spawnOpts = {}, ...serverOpts } = opts;
|
|
88
|
+
const { env, execArgv, stdio = "inherit", startupTimeout = 6e4 } = spawnOpts;
|
|
89
|
+
const workerPath = fileURLToPath(import.meta.resolve("#worker"));
|
|
90
|
+
const childEnv = {
|
|
91
|
+
...process.env,
|
|
92
|
+
...env
|
|
93
|
+
};
|
|
94
|
+
delete childEnv.CODE_SERVER_PARENT_PID;
|
|
95
|
+
const proc = fork(workerPath, {
|
|
96
|
+
stdio: [
|
|
97
|
+
"ignore",
|
|
98
|
+
stdio,
|
|
99
|
+
stdio,
|
|
100
|
+
"ipc"
|
|
101
|
+
],
|
|
102
|
+
env: childEnv,
|
|
103
|
+
...execArgv !== void 0 ? { execArgv } : {}
|
|
104
|
+
});
|
|
105
|
+
const ready = await new Promise((resolve, reject) => {
|
|
106
|
+
const onMessage = (msg) => {
|
|
107
|
+
if (msg?.type === "ready") {
|
|
108
|
+
cleanup();
|
|
109
|
+
resolve(msg);
|
|
110
|
+
} else if (msg?.type === "error") {
|
|
111
|
+
cleanup();
|
|
112
|
+
reject(new Error(msg.message ?? "worker error"));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const onExit = (code) => {
|
|
116
|
+
cleanup();
|
|
117
|
+
reject(/* @__PURE__ */ new Error(`coderaft worker exited before ready (code=${code})`));
|
|
118
|
+
};
|
|
119
|
+
const onError = (err) => {
|
|
120
|
+
cleanup();
|
|
121
|
+
proc.kill("SIGKILL");
|
|
122
|
+
reject(err);
|
|
123
|
+
};
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
cleanup();
|
|
126
|
+
proc.kill("SIGKILL");
|
|
127
|
+
reject(/* @__PURE__ */ new Error(`coderaft worker did not become ready within ${startupTimeout}ms`));
|
|
128
|
+
}, startupTimeout);
|
|
129
|
+
timer.unref?.();
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
proc.off("message", onMessage);
|
|
133
|
+
proc.off("exit", onExit);
|
|
134
|
+
proc.off("error", onError);
|
|
135
|
+
};
|
|
136
|
+
proc.on("message", onMessage);
|
|
137
|
+
proc.once("exit", onExit);
|
|
138
|
+
proc.once("error", onError);
|
|
139
|
+
proc.send({
|
|
140
|
+
type: "start",
|
|
141
|
+
opts: serverOpts
|
|
142
|
+
}, (err) => {
|
|
143
|
+
if (err) {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
proc,
|
|
151
|
+
url: ready.url,
|
|
152
|
+
port: ready.port,
|
|
153
|
+
socketPath: ready.socketPath,
|
|
154
|
+
connectionToken: ready.connectionToken
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function terminateChild(proc) {
|
|
158
|
+
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
159
|
+
await new Promise((resolve) => {
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGKILL");
|
|
162
|
+
}, 5e3);
|
|
163
|
+
timer.unref?.();
|
|
164
|
+
proc.once("exit", () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
proc.kill("SIGTERM");
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
export { SpawnedCodeServer, createCodeServer, spawnCodeServer, startCodeServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderaft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"repository": "pithings/coderaft",
|
|
5
5
|
"bin": {
|
|
6
6
|
"coderaft": "./dist/cli.mjs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"ThirdPartyNotices.txt",
|
|
12
12
|
"tar.mjs",
|
|
13
13
|
"code.mjs",
|
|
14
|
+
"worker.mjs",
|
|
14
15
|
"android-preload.cjs",
|
|
15
16
|
"code.tar.zst"
|
|
16
17
|
],
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
},
|
|
22
23
|
"imports": {
|
|
23
24
|
"#code": "./code.mjs",
|
|
25
|
+
"#worker": "./worker.mjs",
|
|
24
26
|
"#android-preload": "./android-preload.cjs"
|
|
25
27
|
},
|
|
26
28
|
"scripts": {
|
package/worker.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Forked worker entry used by `spawnCodeServer`. Starts a coderaft code server
|
|
2
|
+
// on IPC `{ type: "start", opts }` and replies with `{ type: "ready", ... }`.
|
|
3
|
+
// On SIGTERM/SIGINT, gracefully closes the server before exiting so the parent's
|
|
4
|
+
// `handle.close()` cleanly disposes VS Code and releases sockets/locks.
|
|
5
|
+
import { startCodeServer } from "./dist/index.mjs";
|
|
6
|
+
|
|
7
|
+
let handle;
|
|
8
|
+
let shuttingDown = false;
|
|
9
|
+
|
|
10
|
+
process.on("message", async (msg) => {
|
|
11
|
+
if (msg?.type !== "start") return;
|
|
12
|
+
try {
|
|
13
|
+
handle = await startCodeServer(msg.opts);
|
|
14
|
+
process.send?.({
|
|
15
|
+
type: "ready",
|
|
16
|
+
url: handle.url,
|
|
17
|
+
port: handle.port,
|
|
18
|
+
socketPath: handle.socketPath,
|
|
19
|
+
connectionToken: handle.connectionToken,
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
// Flush the error message over IPC before exiting — `process.exit` can
|
|
23
|
+
// tear down the channel before the message reaches the parent, which
|
|
24
|
+
// would leave the parent rejecting with the generic `exited before ready`.
|
|
25
|
+
const exit = () => process.exit(1);
|
|
26
|
+
if (process.send) {
|
|
27
|
+
process.send({ type: "error", message: err?.message ?? String(err) }, exit);
|
|
28
|
+
} else {
|
|
29
|
+
exit();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const shutdown = async (signal) => {
|
|
35
|
+
if (shuttingDown) return;
|
|
36
|
+
shuttingDown = true;
|
|
37
|
+
try {
|
|
38
|
+
await handle?.close();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`[coderaft worker] close failed on ${signal}:`, err);
|
|
41
|
+
}
|
|
42
|
+
process.exit(0);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
46
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
47
|
+
|
|
48
|
+
// Forward fatal async errors to the parent before exiting. Without this, the
|
|
49
|
+
// parent only sees an anonymous non-zero exit and has to scrape stderr for the
|
|
50
|
+
// reason — which is the exact failure mode the pre-ready IPC flush avoids.
|
|
51
|
+
const forwardFatal = (kind) => (err) => {
|
|
52
|
+
if (shuttingDown) return;
|
|
53
|
+
shuttingDown = true;
|
|
54
|
+
const detail = err?.stack ?? err?.message ?? String(err);
|
|
55
|
+
const exit = () => process.exit(1);
|
|
56
|
+
if (process.send) {
|
|
57
|
+
process.send({ type: "error", message: `[${kind}] ${detail}` }, exit);
|
|
58
|
+
} else {
|
|
59
|
+
exit();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
process.on("uncaughtException", forwardFatal("uncaughtException"));
|
|
64
|
+
process.on("unhandledRejection", forwardFatal("unhandledRejection"));
|