@qilin-agent/cli 3.1.3 → 3.1.4
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.i18n.yaml +14 -14
- package/README.md +12 -3
- package/README.zh.md +12 -3
- package/lib/bin.js +101 -7
- package/lib/tenant-runtime-ChD53FKq.js +489 -0
- package/lib/tenants-roster-Bhr9x2Jw.js +252 -0
- package/lib/types/args.d.ts +10 -1
- package/lib/types/scheduler-yield.d.ts +19 -0
- package/lib/types/tenant-proxy.d.ts +101 -0
- package/lib/types/tenant-runtime.d.ts +338 -0
- package/lib/types/tenants-roster.d.ts +138 -0
- package/lib/types/tenants.d.ts +175 -0
- package/lib/types/usage-ledger.d.ts +143 -0
- package/lib/types/web-entry.d.ts +97 -0
- package/lib/web-entry-C--0C6wG.js +1500 -0
- package/package.json +127 -119
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import "@qilin-agent/http-proxy";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import "@qilin-agent/kylin";
|
|
8
|
+
//#region ../../packages/subprocess/subprocess/src/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Credential-shaped environment names are NOT forwarded to children (the
|
|
11
|
+
* harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a spawned
|
|
12
|
+
* process implicitly). One heuristic for every in-repo spawner; a
|
|
13
|
+
* deliberately supplied entry survives because explicit env layers merge
|
|
14
|
+
* after the scrub.
|
|
15
|
+
*/
|
|
16
|
+
const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region lib/types/tenant-runtime.js
|
|
19
|
+
/**
|
|
20
|
+
* One tenant's Web runtime: its own Harness home under the deployment home, the
|
|
21
|
+
* child process that serves it, and the launch-token URL a browser follows into
|
|
22
|
+
* it.
|
|
23
|
+
*
|
|
24
|
+
* A tenant is a deployment of its own. The child runs with `QILIN_HOME` set to
|
|
25
|
+
* this tenant's directory, so plugins, model settings, credentials, sessions,
|
|
26
|
+
* storages, attachments, skills, and presets all resolve inside it and nowhere
|
|
27
|
+
* else. The only shared file is the deployment's account roster, which the
|
|
28
|
+
* tenant's home layer points at while admitting exactly its own account.
|
|
29
|
+
* @module @qilin-agent/cli/tenant-runtime
|
|
30
|
+
*/
|
|
31
|
+
/** Any startup line whose label is followed by the served URL. */
|
|
32
|
+
const READY_URL = /(?:^|\n)[A-Za-z][\w -]*: (https?:\/\/\S+)/u;
|
|
33
|
+
/** Resolution base for a loader named by a bare specifier. */
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
/**
|
|
36
|
+
* Workspace path map a source-mode child resolves against.
|
|
37
|
+
*
|
|
38
|
+
* A source launch's entry imports workspace packages by name. The child runs in
|
|
39
|
+
* its own tenant directory, where the loader would otherwise fall back to built
|
|
40
|
+
* `lib` output; this is the same map the repository's own subprocess runner
|
|
41
|
+
* gives a `.ts` entry (see `@qilin-agent/subprocess-local`).
|
|
42
|
+
*/
|
|
43
|
+
const SOURCE_TSCONFIG_PATH = fileURLToPath(new URL("../../../tsconfig.base.json", import.meta.url));
|
|
44
|
+
/** Deadline for one tenant runtime to print its URL. */
|
|
45
|
+
const START_DEADLINE_MS = 12e4;
|
|
46
|
+
/** Node options whose value names a module the child must load. */
|
|
47
|
+
const LOADER_FLAGS = new Set([
|
|
48
|
+
"--import",
|
|
49
|
+
"--loader",
|
|
50
|
+
"--experimental-loader",
|
|
51
|
+
"-r",
|
|
52
|
+
"--require"
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Resolve every bare module name in the hub's own Node options to an absolute
|
|
56
|
+
* path.
|
|
57
|
+
*
|
|
58
|
+
* The child starts in its own tenant directory, so a bare specifier such as the
|
|
59
|
+
* `tsx/esm` a source launch passes to `--import` resolves against the wrong
|
|
60
|
+
* tree and the child dies before it prints anything. Resolving here keeps the
|
|
61
|
+
* child on the hub's own loader regardless of where it runs.
|
|
62
|
+
* @param execArgv - the hub's `process.execArgv`.
|
|
63
|
+
* @returns the same options with loader values made absolute.
|
|
64
|
+
*/
|
|
65
|
+
function resolveLoaderArguments(execArgv) {
|
|
66
|
+
const resolved = [];
|
|
67
|
+
for (const [index, argument] of execArgv.entries()) {
|
|
68
|
+
const previous = execArgv[index - 1];
|
|
69
|
+
if (previous !== void 0 && LOADER_FLAGS.has(previous) && !argument.startsWith(".") && !argument.startsWith("/")) try {
|
|
70
|
+
resolved.push(pathToFileURL(require.resolve(argument)).href);
|
|
71
|
+
continue;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!(error instanceof Error)) throw error;
|
|
74
|
+
}
|
|
75
|
+
resolved.push(argument);
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
/** Interface a tenant runtime binds when the deployment names none. */
|
|
80
|
+
const DEFAULT_TENANT_HOST = "127.0.0.1";
|
|
81
|
+
/**
|
|
82
|
+
* One tenant runtime's arguments, after the Node executable.
|
|
83
|
+
*
|
|
84
|
+
* The launcher stops reading its own flags at the first token it does not own,
|
|
85
|
+
* and `--host` belongs to the served application: everything from there on
|
|
86
|
+
* reaches that application. An overlay belongs to the launcher, so it travels
|
|
87
|
+
* before it, and the deployment's own application arguments follow `--no-open`
|
|
88
|
+
* so a runtime never opens a browser of its own.
|
|
89
|
+
* @param launch - the profile, interface, overlays, and application arguments.
|
|
90
|
+
* @returns argv for one child process.
|
|
91
|
+
*/
|
|
92
|
+
function tenantChildArguments(launch) {
|
|
93
|
+
return [
|
|
94
|
+
"--profile",
|
|
95
|
+
launch.profile,
|
|
96
|
+
...launch.patches.flatMap((path) => ["--patch", path]),
|
|
97
|
+
"--host",
|
|
98
|
+
launch.host,
|
|
99
|
+
"--port",
|
|
100
|
+
"0",
|
|
101
|
+
"--no-open",
|
|
102
|
+
...launch.appArguments
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
/** Deadline for one tenant runtime to exit after SIGTERM, before SIGKILL. */
|
|
106
|
+
const STOP_DEADLINE_MS = 15e3;
|
|
107
|
+
/**
|
|
108
|
+
* Ceiling the retry wait reaches. A tenant runtime that cannot start is broken
|
|
109
|
+
* in a way a browser reload does not fix, so the wait grows up to this bound
|
|
110
|
+
* instead of retrying on every visit.
|
|
111
|
+
*/
|
|
112
|
+
const BACKOFF_CEILING_MS = 3e5;
|
|
113
|
+
/**
|
|
114
|
+
* Character set an account identity may use as a tenant directory name. The
|
|
115
|
+
* document each layer carries is built from it, so it never needs quoting.
|
|
116
|
+
*/
|
|
117
|
+
const IDENTITY_PATTERN = /^[A-Za-z0-9._-]+$/u;
|
|
118
|
+
/** Prefix the deployment reserves for directories of its own under `tenants/`. */
|
|
119
|
+
const RESERVED_IDENTITY_PREFIX = ".";
|
|
120
|
+
/**
|
|
121
|
+
* Whether an account identity may name one tenant directory.
|
|
122
|
+
*
|
|
123
|
+
* A leading dot is reserved: the deployment keeps its own directories beside the
|
|
124
|
+
* tenants (`tenants/.usage` holds the token ledger, `tenants/.bootstrap` the
|
|
125
|
+
* first-run runtime), so an identity that read as one would take over the
|
|
126
|
+
* deployment's own bookkeeping instead of a home of its own.
|
|
127
|
+
*
|
|
128
|
+
* The account file is a shared document, so its ids reach path construction in
|
|
129
|
+
* more than one process; an id that is a path segment or names a directory would
|
|
130
|
+
* place a tenant's home somewhere other than under the deployment's own tenants.
|
|
131
|
+
* @param accountId - the identity stored in the roster.
|
|
132
|
+
* @returns true when the identity is one plain directory name.
|
|
133
|
+
*/
|
|
134
|
+
function isTenantIdentity(accountId) {
|
|
135
|
+
return IDENTITY_PATTERN.test(accountId) && !accountId.startsWith(RESERVED_IDENTITY_PREFIX);
|
|
136
|
+
}
|
|
137
|
+
/** Environment variable naming the deployment home to a tenant runtime. */
|
|
138
|
+
const DEPLOYMENT_HOME_VARIABLE = "QILIN_DEPLOYMENT_HOME";
|
|
139
|
+
/** Environment variable naming the agent-config root (user skills and agent documents). */
|
|
140
|
+
const AGENTS_HOME_VARIABLE = "QILIN_AGENTS_HOME";
|
|
141
|
+
/**
|
|
142
|
+
* Environment variable naming the account one tenant runtime serves.
|
|
143
|
+
*
|
|
144
|
+
* The served document carries it back to the browser, and the client keeps its
|
|
145
|
+
* own local storage inside that account's space: one origin can serve several
|
|
146
|
+
* accounts behind this deployment, so a storage name that does not carry an
|
|
147
|
+
* account would be shared by all of them. Absent for the first-run runtime,
|
|
148
|
+
* which serves no account yet.
|
|
149
|
+
*/
|
|
150
|
+
const ACCOUNT_VARIABLE = "QILIN_ACCOUNT_ID";
|
|
151
|
+
/** Environment variable naming the deployment's usage endpoint, when it has one. */
|
|
152
|
+
const USAGE_URL_VARIABLE = "QILIN_USAGE_URL";
|
|
153
|
+
/**
|
|
154
|
+
* Environment variable carrying the token that endpoint accepts.
|
|
155
|
+
*
|
|
156
|
+
* It identifies the runtime that presents it, so the hub books the report to the
|
|
157
|
+
* account that token was minted for rather than to whatever the body claims.
|
|
158
|
+
*/
|
|
159
|
+
const USAGE_TOKEN_VARIABLE = "QILIN_USAGE_TOKEN";
|
|
160
|
+
/**
|
|
161
|
+
* The environment one tenant runtime starts with.
|
|
162
|
+
*
|
|
163
|
+
* The tenant's harness home is the isolation: every root the runtime resolves
|
|
164
|
+
* comes from `QILIN_HOME`. The agent-config root is the exception that has to be
|
|
165
|
+
* named explicitly, because it defaults to the shared `~/.agents` (or whatever
|
|
166
|
+
* the hub inherited), and user skills and agent documents belong to one account.
|
|
167
|
+
* The deployment home arrives by name so the roster layer can read the shared
|
|
168
|
+
* account file; nothing else of the deployment's is exposed.
|
|
169
|
+
*
|
|
170
|
+
* Credential-shaped names do not cross either: a key the hub was launched with
|
|
171
|
+
* authenticates the hub, and handing it to every account would bill one
|
|
172
|
+
* account's requests to whoever started the deployment. A tenant reaches a
|
|
173
|
+
* provider through its own credential record, or through an entry the
|
|
174
|
+
* deployment layers for that tenant on purpose. The rule is the harness's one
|
|
175
|
+
* credential heuristic; every other inherited name, including the deployment's
|
|
176
|
+
* own `QILIN_*` policy, still crosses.
|
|
177
|
+
* @param tenantDir - the tenant's own harness home.
|
|
178
|
+
* @param deploymentHome - the shared deployment home holding the account roster.
|
|
179
|
+
* The account this runtime serves is named too, so the document it hands the
|
|
180
|
+
* browser can say whose storage the page may use.
|
|
181
|
+
* @param base - the environment the hub itself runs with.
|
|
182
|
+
* @param accountId - the account this runtime serves, or undefined for the
|
|
183
|
+
* first-run runtime that serves no account yet.
|
|
184
|
+
* @param usage - endpoint the runtime reports its account's token spend to, and
|
|
185
|
+
* the token that endpoint accepts; absent when the deployment keeps no ledger.
|
|
186
|
+
* @returns the environment mapping for the child process.
|
|
187
|
+
*/
|
|
188
|
+
function tenantChildEnvironment(tenantDir, deploymentHome, base = process.env, accountId, usage) {
|
|
189
|
+
const inherited = {};
|
|
190
|
+
for (const [name, value] of Object.entries(base)) {
|
|
191
|
+
if (value === void 0 || SENSITIVE_ENV_PATTERN.test(name)) continue;
|
|
192
|
+
inherited[name] = value;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...inherited,
|
|
196
|
+
QILIN_HOME: tenantDir,
|
|
197
|
+
...accountId === void 0 ? {} : { [ACCOUNT_VARIABLE]: accountId },
|
|
198
|
+
[TENANT_VARIABLE]: "1",
|
|
199
|
+
[DEPLOYMENT_HOME_VARIABLE]: deploymentHome,
|
|
200
|
+
[AGENTS_HOME_VARIABLE]: join(tenantDir, "agents"),
|
|
201
|
+
...usage === void 0 ? {} : {
|
|
202
|
+
[USAGE_URL_VARIABLE]: usage.url,
|
|
203
|
+
[USAGE_TOKEN_VARIABLE]: usage.token
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Addresses that only the machine itself reaches. */
|
|
208
|
+
const LOOPBACK_HOSTS = [
|
|
209
|
+
"127.0.0.1",
|
|
210
|
+
"::1",
|
|
211
|
+
"localhost"
|
|
212
|
+
];
|
|
213
|
+
/**
|
|
214
|
+
* Marks the process as one account's runtime rather than the entry that serves
|
|
215
|
+
* the public address. The supervisor sets it for every child it starts; a
|
|
216
|
+
* deployment that starts a single-account runtime directly sets it too.
|
|
217
|
+
*/
|
|
218
|
+
const TENANT_VARIABLE = "QILIN_TENANT";
|
|
219
|
+
/**
|
|
220
|
+
* Layer of the runtime a deployment uses before it has any account.
|
|
221
|
+
*
|
|
222
|
+
* That runtime reads and writes the same roster, and admits whoever the roster
|
|
223
|
+
* holds: the product's own first-run form creates the first account, and the
|
|
224
|
+
* hub then serves it from a runtime of its own.
|
|
225
|
+
* @param secureCookies - whether the browser reaches this deployment over
|
|
226
|
+
* TLS, so its account surface marks session cookies `Secure`.
|
|
227
|
+
* @returns the layer document as YAML.
|
|
228
|
+
*/
|
|
229
|
+
function bootstrapPatchYaml(registration = "open", secureCookies = false) {
|
|
230
|
+
return [
|
|
231
|
+
"# First-run layer for a deployment with no accounts yet.",
|
|
232
|
+
"# This runtime admits whoever the roster holds, so the product can create",
|
|
233
|
+
"# the first account and, when the deployment accepts new accounts, the ones",
|
|
234
|
+
"# after it; the hub then routes each of them to a runtime of its own.",
|
|
235
|
+
"- id: accounts",
|
|
236
|
+
" config:",
|
|
237
|
+
` qilinHome: !!js process.env.${DEPLOYMENT_HOME_VARIABLE}`,
|
|
238
|
+
` registration: ${registration}`,
|
|
239
|
+
...secureCookies ? [" sessionCookieSecure: true"] : [],
|
|
240
|
+
""
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
/** Directory name of the runtime a deployment without accounts uses. */
|
|
244
|
+
const BOOTSTRAP_DIRECTORY = ".bootstrap";
|
|
245
|
+
/**
|
|
246
|
+
* One tenant's harness home under the deployment home.
|
|
247
|
+
*
|
|
248
|
+
* The name is one path segment — an account identity, or the first-run
|
|
249
|
+
* directory — so a tenant home is always a sibling of every other one.
|
|
250
|
+
* @param home - deployment home.
|
|
251
|
+
* @param name - account identity or first-run directory name.
|
|
252
|
+
* @returns the tenant's harness home.
|
|
253
|
+
*/
|
|
254
|
+
function tenantDirectory(home, name) {
|
|
255
|
+
return join(home, "tenants", name);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Build one tenant's harness-home layer: the account roster it reads and the
|
|
259
|
+
* single account it admits.
|
|
260
|
+
*
|
|
261
|
+
* The roster stays in the deployment home so one command manages every account,
|
|
262
|
+
* while the tenant's own home holds everything else. The deployment home
|
|
263
|
+
* arrives through the environment rather than as a literal, so a path with
|
|
264
|
+
* quotes or a trailing backslash cannot change the document's meaning.
|
|
265
|
+
* @param accountId - account identity this tenant serves.
|
|
266
|
+
* @param secureCookies - whether the browser reaches this deployment over
|
|
267
|
+
* TLS, so its account surface marks session cookies `Secure`.
|
|
268
|
+
* @returns the layer document as YAML.
|
|
269
|
+
* @throws Error when the identity is not usable as one tenant directory name.
|
|
270
|
+
*/
|
|
271
|
+
function tenantPatchYaml(accountId, secureCookies = false) {
|
|
272
|
+
if (!isTenantIdentity(accountId)) throw new Error(`qilin tenants: ${accountId} is not usable as a tenant directory name`);
|
|
273
|
+
return [
|
|
274
|
+
`# Tenant harness-home layer for account ${accountId}.`,
|
|
275
|
+
"# This runtime admits exactly that account and reads the deployment roster;",
|
|
276
|
+
"# its plugins, model settings, credentials, sessions, and storages live in",
|
|
277
|
+
"# this tenant home.",
|
|
278
|
+
"- id: accounts",
|
|
279
|
+
" config:",
|
|
280
|
+
` qilinHome: !!js process.env.${DEPLOYMENT_HOME_VARIABLE}`,
|
|
281
|
+
" admittedAccounts:",
|
|
282
|
+
` - '${accountId}'`,
|
|
283
|
+
" registration: closed",
|
|
284
|
+
...secureCookies ? [" sessionCookieSecure: true"] : [],
|
|
285
|
+
""
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
var TenantRuntime = class {
|
|
289
|
+
options;
|
|
290
|
+
child;
|
|
291
|
+
exited;
|
|
292
|
+
launchUrl;
|
|
293
|
+
state = "stopped";
|
|
294
|
+
detail = "";
|
|
295
|
+
output = "";
|
|
296
|
+
starting;
|
|
297
|
+
consecutiveFailures = 0;
|
|
298
|
+
retryAt;
|
|
299
|
+
intentional = false;
|
|
300
|
+
/** Epoch milliseconds of the last browser handoff; idle reclamation measures it. */
|
|
301
|
+
lastUsedAt = 0;
|
|
302
|
+
/** How many times this tenant's runtime has started. */
|
|
303
|
+
starts = 0;
|
|
304
|
+
/**
|
|
305
|
+
* @param options - the shared home, the account this runtime serves, and how to start it.
|
|
306
|
+
*/
|
|
307
|
+
constructor(options) {
|
|
308
|
+
this.options = options;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Where the hub sends this runtime's traffic.
|
|
312
|
+
*
|
|
313
|
+
* The runtime's own URL carries a launch token, which the hub never spends:
|
|
314
|
+
* every request keeps the path the browser asked for, so the application's
|
|
315
|
+
* account gate answers it. Only the origin is a target.
|
|
316
|
+
* @returns the runtime's origin, or undefined when it is not serving yet.
|
|
317
|
+
*/
|
|
318
|
+
proxyTarget() {
|
|
319
|
+
if (this.launchUrl === void 0) return void 0;
|
|
320
|
+
return new URL(this.launchUrl).origin;
|
|
321
|
+
}
|
|
322
|
+
/** Absolute path of this tenant's harness-home layer. */
|
|
323
|
+
get patchPath() {
|
|
324
|
+
return join(this.tenantDir, "cordis.patch.yml");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* This tenant's harness home: everything the runtime owns lives under it,
|
|
328
|
+
* and the deployment home holds only the roster and these directories.
|
|
329
|
+
*/
|
|
330
|
+
get tenantDir() {
|
|
331
|
+
return tenantDirectory(this.options.home, this.options.directoryName ?? this.options.accountId);
|
|
332
|
+
}
|
|
333
|
+
/** What the hub's status read reports about this tenant. */
|
|
334
|
+
status() {
|
|
335
|
+
return {
|
|
336
|
+
accountId: this.options.accountId,
|
|
337
|
+
state: this.state,
|
|
338
|
+
pid: this.child?.pid,
|
|
339
|
+
origin: this.launchUrl === void 0 ? void 0 : new URL(this.launchUrl).origin,
|
|
340
|
+
starts: this.starts,
|
|
341
|
+
detail: this.detail,
|
|
342
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
343
|
+
retryAt: this.retryAt
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Return this tenant's launch URL, starting the runtime when it is not serving.
|
|
348
|
+
* @returns the launch-token URL of the running runtime.
|
|
349
|
+
* @throws Error when the runtime did not become ready; the state then reports `failed`.
|
|
350
|
+
*/
|
|
351
|
+
async ensure() {
|
|
352
|
+
this.lastUsedAt = Date.now();
|
|
353
|
+
if (this.state === "running" && this.launchUrl !== void 0) return this.launchUrl;
|
|
354
|
+
const retryAt = this.retryAt;
|
|
355
|
+
if (this.consecutiveFailures > 0 && retryAt !== void 0 && Date.now() < retryAt) throw new Error(`qilin tenants: the runtime for ${this.options.accountId} failed ${String(this.consecutiveFailures)} time(s) and is not retried before ${new Date(retryAt).toISOString()}: ${this.detail}`);
|
|
356
|
+
this.starting ??= this.start();
|
|
357
|
+
try {
|
|
358
|
+
return await this.starting;
|
|
359
|
+
} finally {
|
|
360
|
+
this.starting = void 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Count one failed start or one crash of a runtime that had been serving, and
|
|
365
|
+
* schedule the earliest time the next attempt may run.
|
|
366
|
+
* @param reason - what to report as the current detail.
|
|
367
|
+
*/
|
|
368
|
+
recordFailure(reason) {
|
|
369
|
+
this.detail = reason;
|
|
370
|
+
this.consecutiveFailures += 1;
|
|
371
|
+
const wait = Math.min(this.options.restartBackoffMilliseconds * 2 ** (this.consecutiveFailures - 1), BACKOFF_CEILING_MS);
|
|
372
|
+
this.retryAt = Date.now() + wait;
|
|
373
|
+
}
|
|
374
|
+
/** Ask this tenant's runtime to exit, holding it to the shutdown deadline. */
|
|
375
|
+
async stop() {
|
|
376
|
+
const child = this.child;
|
|
377
|
+
const exited = this.exited;
|
|
378
|
+
if (child === void 0 || exited === void 0) {
|
|
379
|
+
if (this.state !== "failed") this.state = "stopped";
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
this.intentional = true;
|
|
383
|
+
child.kill("SIGTERM");
|
|
384
|
+
const killed = setTimeout(() => {
|
|
385
|
+
child.kill("SIGKILL");
|
|
386
|
+
}, STOP_DEADLINE_MS);
|
|
387
|
+
try {
|
|
388
|
+
await exited;
|
|
389
|
+
} finally {
|
|
390
|
+
clearTimeout(killed);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** Start one child, wait for its URL, and keep its handles. */
|
|
394
|
+
async start() {
|
|
395
|
+
await mkdir(this.tenantDir, { recursive: true });
|
|
396
|
+
await this.writePatch();
|
|
397
|
+
this.state = "starting";
|
|
398
|
+
this.detail = "";
|
|
399
|
+
this.output = "";
|
|
400
|
+
this.launchUrl = void 0;
|
|
401
|
+
this.starts += 1;
|
|
402
|
+
const child = spawn(process.execPath, [
|
|
403
|
+
...resolveLoaderArguments(this.options.execArgv),
|
|
404
|
+
this.options.binPath,
|
|
405
|
+
...tenantChildArguments({
|
|
406
|
+
profile: this.options.profile,
|
|
407
|
+
host: this.options.host ?? DEFAULT_TENANT_HOST,
|
|
408
|
+
patches: this.options.patches,
|
|
409
|
+
appArguments: this.options.appArguments
|
|
410
|
+
})
|
|
411
|
+
], {
|
|
412
|
+
cwd: this.tenantDir,
|
|
413
|
+
env: {
|
|
414
|
+
...tenantChildEnvironment(this.tenantDir, this.options.home, process.env, this.options.bootstrap === true ? void 0 : this.options.accountId, this.options.bootstrap === true ? void 0 : this.options.usage),
|
|
415
|
+
...this.options.binPath.endsWith(".ts") ? { TSX_TSCONFIG_PATH: SOURCE_TSCONFIG_PATH } : {}
|
|
416
|
+
},
|
|
417
|
+
stdio: [
|
|
418
|
+
"ignore",
|
|
419
|
+
"pipe",
|
|
420
|
+
"pipe"
|
|
421
|
+
]
|
|
422
|
+
});
|
|
423
|
+
const ready = Promise.withResolvers();
|
|
424
|
+
let banner = "";
|
|
425
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
426
|
+
banner += chunk;
|
|
427
|
+
const lines = banner.split("\n");
|
|
428
|
+
banner = lines.pop() ?? "";
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
const found = READY_URL.exec(line)?.[1];
|
|
431
|
+
if (found !== void 0) ready.resolve(found);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
let pending = "";
|
|
435
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
436
|
+
this.output = `${this.output}${chunk}`.slice(-64e3);
|
|
437
|
+
pending += chunk;
|
|
438
|
+
const lines = pending.split("\n");
|
|
439
|
+
pending = lines.pop() ?? "";
|
|
440
|
+
for (const line of lines) if (line.trim() !== "") this.options.log?.(line);
|
|
441
|
+
});
|
|
442
|
+
const exited = Promise.withResolvers();
|
|
443
|
+
child.once("error", (error) => {
|
|
444
|
+
this.output = `${this.output}\nspawn failed: ${String(error)}`.slice(-64e3);
|
|
445
|
+
});
|
|
446
|
+
child.once("close", (code) => {
|
|
447
|
+
if (this.child !== child) return;
|
|
448
|
+
const crashed = this.launchUrl !== void 0 && !this.intentional;
|
|
449
|
+
this.child = void 0;
|
|
450
|
+
this.launchUrl = void 0;
|
|
451
|
+
this.exited = void 0;
|
|
452
|
+
this.intentional = false;
|
|
453
|
+
this.state = crashed ? "failed" : "stopped";
|
|
454
|
+
if (crashed) this.recordFailure(`the runtime for ${this.options.accountId} exited with code ${String(code)} after it was ready`);
|
|
455
|
+
ready.reject(/* @__PURE__ */ new Error(`qilin tenants: the runtime for ${this.options.accountId} exited before its URL\n${this.output.slice(-2e3)}`));
|
|
456
|
+
exited.resolve();
|
|
457
|
+
});
|
|
458
|
+
this.child = child;
|
|
459
|
+
this.exited = exited.promise;
|
|
460
|
+
const deadline = setTimeout(() => {
|
|
461
|
+
ready.reject(/* @__PURE__ */ new Error(`qilin tenants: the runtime for ${this.options.accountId} printed no URL within ${String(START_DEADLINE_MS)}ms\n${this.output}`));
|
|
462
|
+
}, START_DEADLINE_MS);
|
|
463
|
+
try {
|
|
464
|
+
const url = await ready.promise;
|
|
465
|
+
this.launchUrl = url;
|
|
466
|
+
this.state = "running";
|
|
467
|
+
this.consecutiveFailures = 0;
|
|
468
|
+
this.retryAt = void 0;
|
|
469
|
+
this.detail = "";
|
|
470
|
+
return url;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
this.state = "failed";
|
|
473
|
+
this.recordFailure(error instanceof Error ? error.message : String(error));
|
|
474
|
+
await this.stop();
|
|
475
|
+
throw error;
|
|
476
|
+
} finally {
|
|
477
|
+
clearTimeout(deadline);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Write this tenant's home layer when the file does not already hold it. */
|
|
481
|
+
async writePatch() {
|
|
482
|
+
const secureCookies = this.options.sessionCookieSecure === true;
|
|
483
|
+
const document = this.options.bootstrap === true ? bootstrapPatchYaml(this.options.registration ?? "open", secureCookies) : tenantPatchYaml(this.options.accountId, secureCookies);
|
|
484
|
+
if (await readFile(this.patchPath, "utf8").catch(() => void 0) === document) return;
|
|
485
|
+
await writeFile(this.patchPath, document);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
//#endregion
|
|
489
|
+
export { isTenantIdentity as a, TenantRuntime as i, LOOPBACK_HOSTS as n, TENANT_VARIABLE as r, BOOTSTRAP_DIRECTORY as t };
|