@volter/world-runtime 3.0.163 → 3.0.164
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/dist/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +3 -2
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +445 -18
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +4 -3
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +416 -17
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ensureCa, leafCertFor, proxyAddressEnv } from "./proxy-env.js";
|
|
2
|
+
export { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, tearDownCa, leafCertFor, proxyEnvFor } from "./proxy-env.js";
|
|
1
3
|
import { errorMessage } from "./error-message.js";
|
|
2
4
|
import { serveHttp } from '@volter/world-core/runtime';
|
|
3
5
|
import { worldNetworkPolicyFromEnv, worldGrantFromEnv, worldStrictEgress, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } from '@volter/world-core/network-policy';
|
|
@@ -22,8 +24,6 @@ import { websocketRelay } from "./websocket-relay.js";
|
|
|
22
24
|
//
|
|
23
25
|
// Cert tooling: we shell out to `openssl` (already a dev prerequisite; node:crypto can generate keys
|
|
24
26
|
// but cannot mint X.509 certs without a third-party lib). This keeps world-runtime dependency-light.
|
|
25
|
-
import { spawnSync } from 'node:child_process';
|
|
26
|
-
import { createHash } from 'node:crypto';
|
|
27
27
|
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
28
28
|
import { BlockList, isIP, connect as netConnect, createServer as createNetServer } from 'node:net';
|
|
29
29
|
import { networkInterfaces } from 'node:os';
|
|
@@ -31,8 +31,7 @@ import { connect as tlsConnect } from 'node:tls';
|
|
|
31
31
|
import { request as httpsRequest } from 'node:https';
|
|
32
32
|
import { createServer as createHttpServer } from 'node:http';
|
|
33
33
|
import { Readable } from 'node:stream';
|
|
34
|
-
import {
|
|
35
|
-
import { join } from 'node:path';
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
36
35
|
// The host→twin table is loaded through the shared inject-map module (one world-runtime home
|
|
37
36
|
// for the injector's data), so this proxy, `covers`, and `up`'s inert-injectEnv warning can
|
|
38
37
|
// never re-encode the vendor host predicates — they stay one source of truth with the Node
|
|
@@ -77,10 +76,6 @@ export function proxyTargetFor(hostname, env, pathname) {
|
|
|
77
76
|
const inject = loadInject();
|
|
78
77
|
return inject.resolveTwin(hostname, inject.readMap(env), pathname);
|
|
79
78
|
}
|
|
80
|
-
/** The vendor→twin map this world actively redirects (for diagnostics / activate output). */
|
|
81
|
-
export function activeVendorMap(env) {
|
|
82
|
-
return loadInject().readMap(env);
|
|
83
|
-
}
|
|
84
79
|
/** A connection from anything but the proxy host's own loopback is a guest's (a World Machine: its forwarder connects
|
|
85
80
|
* from 127.0.0.2 on this host, or from its VM's address): architecture, "The scoped proxy's local pass-through". */
|
|
86
81
|
export function isGuestSource(address) {
|
|
@@ -197,91 +192,6 @@ function guestPinRefusal(target) {
|
|
|
197
192
|
return { outcome: 'refuse', capability: 'network.egress', target, missing: 'transport', rule: 'guest-inspected',
|
|
198
193
|
reason: 'inspected egress cannot keep a guest\'s checked destination pin' };
|
|
199
194
|
}
|
|
200
|
-
function caPaths(tlsDir) {
|
|
201
|
-
return { dir: tlsDir, caKey: join(tlsDir, 'ca-key.pem'), caCert: join(tlsDir, 'ca-cert.pem') };
|
|
202
|
-
}
|
|
203
|
-
/** Whether this host opens raw sockets to other hosts, which the ambient TLS redirect proxy exists to intercept. A
|
|
204
|
-
* browser tab's Node (tabnode, which declares `process.versions.webcontainer`) opens none: its TLS is loopback pairing
|
|
205
|
-
* with no handshake, and a remote HTTPS call leaves through the page's HTTP transport. There no proxy can run and none
|
|
206
|
-
* is needed: the World's network policy is enforced by the injector in every Node program (world-core inject.cjs). */
|
|
207
|
-
export function hostHasRawEgress(versions = process.versions) {
|
|
208
|
-
return versions.webcontainer === undefined;
|
|
209
|
-
}
|
|
210
|
-
export function opensslAvailable() {
|
|
211
|
-
// TEST-ONLY seam (TWIN-64): forces "openssl unavailable" deterministically. A PATH-shim (fake
|
|
212
|
-
// `openssl` on PATH) doesn't reliably work here because `spawnSync('openssl', …)` below is called
|
|
213
|
-
// with no explicit `env`, and some runtimes resolve the executable against a PATH snapshotted at
|
|
214
|
-
// process start rather than a same-process `process.env.PATH` mutation made mid-test. Never set
|
|
215
|
-
// this env var in real usage.
|
|
216
|
-
if (process.env.VOLTER_TEST_NO_OPENSSL === '1')
|
|
217
|
-
return false;
|
|
218
|
-
const r = spawnSync('openssl', ['version'], { windowsHide: true, stdio: 'ignore' });
|
|
219
|
-
return r.status === 0;
|
|
220
|
-
}
|
|
221
|
-
function runOpenssl(args) {
|
|
222
|
-
const r = spawnSync('openssl', args, { windowsHide: true, encoding: 'utf8' });
|
|
223
|
-
if (r.status !== 0)
|
|
224
|
-
throw new Error(`openssl ${args[0]} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
225
|
-
return r.stdout;
|
|
226
|
-
}
|
|
227
|
-
/** Generate (once) a throwaway CA in `tlsDir`. Idempotent: reuses an existing CA so a re-activate in
|
|
228
|
-
* the same world trusts the same cert. Session/project-scoped — it lives under the instance dir and
|
|
229
|
-
* is removed by `tearDownCa` on `down`. */
|
|
230
|
-
export function ensureCa(tlsDir) {
|
|
231
|
-
if (!opensslAvailable()) {
|
|
232
|
-
throw new Error('openssl not found — the TLS redirect proxy needs openssl to mint a session CA. Install openssl, or use per-CLI endpoint env (cliRedirect) instead.');
|
|
233
|
-
}
|
|
234
|
-
const paths = caPaths(tlsDir);
|
|
235
|
-
if (existsSync(paths.caKey) && existsSync(paths.caCert))
|
|
236
|
-
return paths;
|
|
237
|
-
mkdirSync(tlsDir, { recursive: true });
|
|
238
|
-
runOpenssl(['genrsa', '-out', paths.caKey, '2048']);
|
|
239
|
-
runOpenssl([
|
|
240
|
-
'req', '-x509', '-new', '-nodes', '-key', paths.caKey,
|
|
241
|
-
'-sha256', '-days', '3650', '-out', paths.caCert,
|
|
242
|
-
'-subj', '/CN=volter-world session CA/O=volter-world',
|
|
243
|
-
// A CA cert without keyUsage is refused by OpenSSL 3 (Python, curl on Debian 13: "CA cert does not
|
|
244
|
-
// include key usage extension"), so the CA states what a CA is for.
|
|
245
|
-
'-addext', 'basicConstraints=critical,CA:TRUE',
|
|
246
|
-
'-addext', 'keyUsage=critical,keyCertSign,cRLSign',
|
|
247
|
-
'-addext', 'subjectKeyIdentifier=hash',
|
|
248
|
-
]);
|
|
249
|
-
return paths;
|
|
250
|
-
}
|
|
251
|
-
/** Remove the session CA + any minted leaf certs. Called on `down` so the trusted CA never outlives
|
|
252
|
-
* the world (it was only ever trusted via the per-shell env, never system-wide). */
|
|
253
|
-
export function tearDownCa(tlsDir) {
|
|
254
|
-
leafCache.clear();
|
|
255
|
-
rmSync(tlsDir, { recursive: true, force: true });
|
|
256
|
-
}
|
|
257
|
-
const leafCache = new Map();
|
|
258
|
-
/** Mint (and cache) a leaf cert for `host`, signed by the session CA. SAN = the host so the client
|
|
259
|
-
* accepts it for that SNI. */
|
|
260
|
-
export function leafCertFor(host, ca) {
|
|
261
|
-
// A pathname can hold a replacement CA; its certificate identifies the signing generation.
|
|
262
|
-
const cacheKey = `${createHash('sha256').update(readFileSync(ca.caCert)).digest('hex')}:${host}`;
|
|
263
|
-
const cached = leafCache.get(cacheKey);
|
|
264
|
-
if (cached)
|
|
265
|
-
return cached;
|
|
266
|
-
const safe = host.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
267
|
-
const keyPath = join(ca.dir, `leaf-${safe}-key.pem`);
|
|
268
|
-
const csrPath = join(ca.dir, `leaf-${safe}.csr`);
|
|
269
|
-
const certPath = join(ca.dir, `leaf-${safe}.pem`);
|
|
270
|
-
const extPath = join(ca.dir, `leaf-${safe}.ext`);
|
|
271
|
-
runOpenssl(['genrsa', '-out', keyPath, '2048']);
|
|
272
|
-
runOpenssl(['req', '-new', '-key', keyPath, '-out', csrPath, '-subj', `/CN=${host}`]);
|
|
273
|
-
writeFileSync(extPath, `subjectAltName=DNS:${host}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`);
|
|
274
|
-
runOpenssl([
|
|
275
|
-
// 397 days, NOT the CA's 3650: macOS's system verifier (used by Go — `gh`, and any Go/Swift
|
|
276
|
-
// client) rejects a LEAF cert valid >398 days as "certificate is not standards compliant"
|
|
277
|
-
// (CA/Browser-Forum max). The session CA itself is exempt and stays long-lived.
|
|
278
|
-
'x509', '-req', '-in', csrPath, '-CA', ca.caCert, '-CAkey', ca.caKey,
|
|
279
|
-
'-CAcreateserial', '-out', certPath, '-days', '397', '-sha256', '-extfile', extPath,
|
|
280
|
-
]);
|
|
281
|
-
const minted = { key: readFileSync(keyPath, 'utf8'), cert: readFileSync(certPath, 'utf8') };
|
|
282
|
-
leafCache.set(cacheKey, minted);
|
|
283
|
-
return minted;
|
|
284
|
-
}
|
|
285
195
|
/** A failed twin request's answer: the twin, its origin, the World and the cause, so the app's error says which service
|
|
286
196
|
* of which World to look at (`doctor`). Shared by every front that relays to twins. */
|
|
287
197
|
export function twinUnreachableMessage(vendor, origin, err, world) {
|
|
@@ -853,22 +763,3 @@ export async function startRedirectProxy(options) {
|
|
|
853
763
|
}),
|
|
854
764
|
};
|
|
855
765
|
}
|
|
856
|
-
/** The cert-trust + proxy env that points an unmodified tool through the proxy and trusts the session
|
|
857
|
-
* CA — and ONLY this session (these are per-shell exports; nothing is installed system-wide). Mirrors
|
|
858
|
-
* the env list in WORLD_ACTIVATE.md §"The honest cost: a scoped CA". */
|
|
859
|
-
export function proxyEnvFor(proxyUrl, caCertPath, caller = process.env, guestUrl) {
|
|
860
|
-
return { ...proxyAddressEnv(proxyUrl, caCertPath, guestUrl), ...sessionTrustEnv(caCertPath, caller) };
|
|
861
|
-
}
|
|
862
|
-
/** The proxy's address env; `VOLTER_WORLD_GUEST_PROXY` is its guests' listener, which a World Machine's forwarder
|
|
863
|
-
* targets (architecture, "The scoped proxy's local pass-through"). */
|
|
864
|
-
function proxyAddressEnv(proxyUrl, caCertPath, guestUrl) {
|
|
865
|
-
return {
|
|
866
|
-
...(guestUrl ? { VOLTER_WORLD_GUEST_PROXY: guestUrl } : {}),
|
|
867
|
-
HTTPS_PROXY: proxyUrl,
|
|
868
|
-
HTTP_PROXY: proxyUrl,
|
|
869
|
-
https_proxy: proxyUrl,
|
|
870
|
-
http_proxy: proxyUrl,
|
|
871
|
-
VOLTER_WORLD_PROXY: proxyUrl,
|
|
872
|
-
VOLTER_WORLD_CA: caCertPath,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type ManagedProtocol } from './twin-backing.js';
|
|
1
2
|
import type { HostLaunch, InfraService } from './pglite-backing.js';
|
|
3
|
+
export declare const REDIS_PROTOCOL: ManagedProtocol;
|
|
2
4
|
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
3
|
-
export declare
|
|
5
|
+
export declare const redisTwinCli: (from: string[]) => string | undefined;
|
|
4
6
|
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
5
7
|
export declare const redisTwinRoot: (dataDir: string) => string;
|
|
6
|
-
export declare
|
|
7
|
-
export declare
|
|
8
|
-
export declare
|
|
8
|
+
export declare const redisTwinUp: (services: InfraService[], dataDir: string, cli: string, launch?: HostLaunch) => Promise<void>;
|
|
9
|
+
export declare const redisTwinStatus: (services: InfraService[], dataDir: string) => Promise<number>;
|
|
10
|
+
export declare const redisTwinDown: (services: InfraService[], dataDir: string) => Promise<void>;
|
|
@@ -8,43 +8,13 @@
|
|
|
8
8
|
// in a container is state outside it (a branch of the World would share the container's queue and cache). The
|
|
9
9
|
// twin keeps each key a subject of the World (the Redis pack's storage module). It needs nothing
|
|
10
10
|
// installed: no container runtime, no redis-server binary.
|
|
11
|
-
|
|
11
|
+
//
|
|
12
|
+
// The lifecycle (start, ownership, readiness, retirement) is every twin-served service's and lives in
|
|
13
|
+
// twin-backing.ts, and the pack that serves the kind is found there from pack facts (`managedService`), not named
|
|
14
|
+
// here. This file is what is Redis's own in the runtime: the two questions asked in Redis's protocol.
|
|
12
15
|
import { connect } from 'node:net';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, signalOwnedProcess } from "./process-receipt.js";
|
|
16
|
-
import { spawn } from 'node:child_process';
|
|
17
|
-
import { survivingOwnedGroups } from "./process-groups.js";
|
|
18
|
-
import { errorMessage } from "./error-message.js";
|
|
19
|
-
import { findInstalledPackage, packCli } from "./catalog.js";
|
|
20
|
-
import { vendorPackageDir } from '@volter/world-core/runtime';
|
|
21
|
-
const hosts = new Map();
|
|
22
|
-
const PACKAGE = '@volter/twin-redis';
|
|
23
|
-
const READY_TIMEOUT_MS = 60_000;
|
|
24
|
-
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
25
|
-
export function redisTwinCli(from) {
|
|
26
|
-
for (const dir of from) {
|
|
27
|
-
const found = vendorPackageDir(dir, 'redis') ?? findInstalledPackage(dir, PACKAGE);
|
|
28
|
-
const cli = found === undefined ? undefined : packCli(found);
|
|
29
|
-
if (cli !== undefined && existsSync(cli))
|
|
30
|
-
return cli;
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
const pidPath = (dataDir, service) => join(dataDir, `redis-twin-${service.hostPort}.pid`);
|
|
35
|
-
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
36
|
-
export const redisTwinRoot = (dataDir) => join(dataDir, 'redis-twin');
|
|
37
|
-
function alive(pid) {
|
|
38
|
-
try {
|
|
39
|
-
process.kill(pid, 0);
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (error.code === 'ESRCH')
|
|
44
|
-
return false;
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { managedDirectory, managedTwin, twinBackingCli, twinBackingDown, twinBackingStatus, twinBackingUp } from "./twin-backing.js";
|
|
48
18
|
function listening(port) {
|
|
49
19
|
return new Promise((resolve) => {
|
|
50
20
|
const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
|
|
@@ -82,135 +52,17 @@ function redisServes(port, pid) {
|
|
|
82
52
|
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
83
53
|
});
|
|
84
54
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
mkdirSync(dirname(log), { recursive: true });
|
|
100
|
-
// a real fd, not a pipe: the server outlives this process untethered
|
|
101
|
-
const logFd = openSync(log, 'a');
|
|
102
|
-
// the runtime this runs on: Bun runs the pack's TypeScript as it is; Node (volter-world-infra's shebang) needs
|
|
103
|
-
// type transformation, not only stripping (the kernel declares constructor parameter properties)
|
|
104
|
-
const runtime = typeof globalThis.Bun !== 'undefined' || !cli.endsWith('.ts') ? [] : ['--experimental-transform-types', '--no-warnings'];
|
|
105
|
-
let held;
|
|
106
|
-
try {
|
|
107
|
-
try {
|
|
108
|
-
const child = spawn(process.execPath, [...runtime, cli, 'serve', '--port', String(service.hostPort), '--root', root], {
|
|
109
|
-
env: launch.env ?? process.env, detached: true, windowsHide: true, stdio: ['ignore', logFd, logFd],
|
|
110
|
-
});
|
|
111
|
-
// Retain BEFORE descriptor close, birth capture or either publication can fail.
|
|
112
|
-
held = { child, lifetime: commandLifetime(child, launch.signal), closed: false };
|
|
113
|
-
const observed = held;
|
|
114
|
-
child.once('close', () => { observed.closed = true; });
|
|
115
|
-
child.on('error', () => { }); // lifetime joins spawn failure and close
|
|
116
|
-
hosts.set(pidFile, held);
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
closeSync(logFd);
|
|
120
|
-
}
|
|
121
|
-
if (!held.child.pid)
|
|
122
|
-
throw new Error('Redis child did not start');
|
|
123
|
-
const identity = await held.lifetime.identity;
|
|
124
|
-
if (!identity?.start)
|
|
125
|
-
throw new Error('Redis birth capture failed; retiring held child');
|
|
126
|
-
publishProcessReceipt(spawnReceiptPath(pidFile), JSON.stringify(identity) + '\n');
|
|
127
|
-
publishProcessReceipt(pidFile, `${held.child.pid}\n`);
|
|
128
|
-
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
129
|
-
while (!alive(held.child.pid) || !(await redisServes(service.hostPort, held.child.pid))) {
|
|
130
|
-
if (!alive(held.child.pid))
|
|
131
|
-
throw new Error(`redis exited before serving — see ${log}`);
|
|
132
|
-
launch.signal?.throwIfAborted();
|
|
133
|
-
if (Date.now() > deadline)
|
|
134
|
-
throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
|
|
135
|
-
await sleep(25);
|
|
136
|
-
}
|
|
137
|
-
launch.signal?.throwIfAborted();
|
|
138
|
-
held.child.unref();
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
if (held) {
|
|
142
|
-
try {
|
|
143
|
-
await redisTwinDown([service], dataDir);
|
|
144
|
-
}
|
|
145
|
-
catch (cleanup) {
|
|
146
|
-
throw new AggregateError([error, cleanup], 'Redis startup and retirement failed; ownership retained');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
export async function redisTwinStatus(services, dataDir) {
|
|
154
|
-
let ready = 0;
|
|
155
|
-
for (const service of services) {
|
|
156
|
-
const pidFile = pidPath(dataDir, service);
|
|
157
|
-
if (!existsSync(pidFile))
|
|
158
|
-
continue;
|
|
159
|
-
const pid = Number(readFileSync(pidFile, 'utf8').trim());
|
|
160
|
-
if (alive(pid) && await redisServes(service.hostPort, pid))
|
|
161
|
-
ready += 1;
|
|
162
|
-
}
|
|
163
|
-
return ready;
|
|
164
|
-
}
|
|
165
|
-
export async function redisTwinDown(services, dataDir) {
|
|
166
|
-
for (const service of services) {
|
|
167
|
-
const pidFile = pidPath(dataDir, service);
|
|
168
|
-
const recorded = readProcessReceipt(pidFile, text => {
|
|
169
|
-
const pid = Number(text.trim());
|
|
170
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
171
|
-
throw new Error('Invalid Redis ownership PID');
|
|
172
|
-
return pid;
|
|
173
|
-
});
|
|
174
|
-
const identity = readSpawnReceipt(pidFile);
|
|
175
|
-
const held = hosts.get(pidFile);
|
|
176
|
-
if (recorded.error || identity.error)
|
|
177
|
-
process.stderr.write(`${errorMessage(new AggregateError([recorded.error, identity.error].filter(Boolean), 'Redis ownership uncertain; retrying established ownership'))}\n`);
|
|
178
|
-
if (held) {
|
|
179
|
-
const retired = () => held.closed && (!held.child.pid || !survivingOwnedGroups([held.child.pid]).length);
|
|
180
|
-
if (!retired()) {
|
|
181
|
-
held.child.ref();
|
|
182
|
-
held.lifetime.cancel('SIGTERM');
|
|
183
|
-
try {
|
|
184
|
-
await held.lifetime.finish();
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
if (!retired())
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
if (!recorded.present && !identity.present)
|
|
194
|
-
continue;
|
|
195
|
-
const pid = identity.value?.pid ?? recorded.value;
|
|
196
|
-
if (!pid)
|
|
197
|
-
throw new AggregateError([recorded.error, identity.error].filter(Boolean), 'Redis ownership uncertain; receipts retained');
|
|
198
|
-
if (recorded.value && identity.value && recorded.value !== identity.value.pid)
|
|
199
|
-
throw new Error('Redis ownership receipts disagree; retirement refused; receipts retained');
|
|
200
|
-
const live = () => survivingOwnedGroups([pid]).length > 0;
|
|
201
|
-
for (const [signal, limit] of [['SIGTERM', COMMAND_GRACE_MS], ['SIGKILL', COMMAND_CONFIRM_MS]]) {
|
|
202
|
-
if (!live())
|
|
203
|
-
break;
|
|
204
|
-
await signalOwnedProcess(identity.value, signal, true);
|
|
205
|
-
const deadline = Date.now() + limit;
|
|
206
|
-
while (live() && Date.now() < deadline)
|
|
207
|
-
await sleep(20);
|
|
208
|
-
}
|
|
209
|
-
if (live())
|
|
210
|
-
throw new Error('Redis retirement unconfirmed; receipts retained');
|
|
211
|
-
}
|
|
212
|
-
rmSync(pidFile, { force: true });
|
|
213
|
-
rmSync(spawnReceiptPath(pidFile), { force: true });
|
|
214
|
-
hosts.delete(pidFile);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
55
|
+
export const REDIS_PROTOCOL = { name: 'Redis', answers: listening, serves: redisServes };
|
|
56
|
+
const twin = () => {
|
|
57
|
+
const found = managedTwin('redis', REDIS_PROTOCOL);
|
|
58
|
+
if (!found)
|
|
59
|
+
throw new Error('no pack declares the redis kind (managedService) in pack facts');
|
|
60
|
+
return found;
|
|
61
|
+
};
|
|
62
|
+
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
63
|
+
export const redisTwinCli = (from) => { const found = managedTwin('redis', REDIS_PROTOCOL); return found && twinBackingCli(found, from); };
|
|
64
|
+
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
65
|
+
export const redisTwinRoot = (dataDir) => join(dataDir, managedDirectory('redis'));
|
|
66
|
+
export const redisTwinUp = (services, dataDir, cli, launch = {}) => twinBackingUp(twin(), services, dataDir, cli, launch);
|
|
67
|
+
export const redisTwinStatus = (services, dataDir) => twinBackingStatus(twin(), services, dataDir);
|
|
68
|
+
export const redisTwinDown = (services, dataDir) => twinBackingDown(twin(), services, dataDir);
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -255,6 +255,7 @@ export declare function attachWorld(name: string, command: string[], root?: stri
|
|
|
255
255
|
cwd?: string;
|
|
256
256
|
owner?: string;
|
|
257
257
|
verbose?: boolean;
|
|
258
|
+
instance?: WorldInstance;
|
|
258
259
|
}): Promise<number>;
|
|
259
260
|
/** The shell + env + cwd a `volter-world shell` subshell launches with (extracted for testability):
|
|
260
261
|
* the world env (twin `*_URL`s + fake keys + the Node injector + cliRedirect endpoint vars) on top
|