@linghun/tools 0.1.3 → 0.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.
@@ -1,678 +0,0 @@
1
- // src/tools/Bash/service-kernel.ts
2
- import { spawn } from "child_process";
3
- import { createWriteStream } from "fs";
4
- import { readFile } from "fs/promises";
5
- import { request as httpRequest } from "http";
6
- import { request as httpsRequest } from "https";
7
- import { connect as connectTcp } from "net";
8
- import { randomUUID } from "crypto";
9
- var DEFAULT_READY_TIMEOUT_MS = 1e4;
10
- var DEFAULT_READY_INTERVAL_MS = 100;
11
- var CONNECT_ATTEMPT_TIMEOUT_MS = 500;
12
- var FETCH_BODY_LIMIT_BYTES = 64e3;
13
- var MAX_MANAGED_SERVICES = 20;
14
- var DEFAULT_LOG_TAIL_BYTES = 4e3;
15
- var STOP_TERM_WAIT_MS = 900;
16
- var STOP_KILL_WAIT_MS = 500;
17
- async function startBashService(input) {
18
- const startedAt = Date.now();
19
- const startedAtIso = new Date(startedAt).toISOString();
20
- const serviceId = `svc_${randomUUID()}`;
21
- const detached = process.platform !== "win32";
22
- const child = spawn(input.command, {
23
- cwd: input.cwd,
24
- shell: true,
25
- windowsHide: true,
26
- detached
27
- });
28
- const log = createWriteStream(input.fullOutputPath, { flags: "w" });
29
- let closed = false;
30
- let exitCode = null;
31
- let signalCode = null;
32
- let lastOutput = "";
33
- let logClosed = false;
34
- const targetFields = readReadinessTargetFields(input.readiness);
35
- const serviceRecord = {
36
- serviceId,
37
- pid: child.pid,
38
- cwd: input.cwd,
39
- command: input.logCommand,
40
- logPath: input.fullOutputPath,
41
- ...targetFields,
42
- readiness: input.readiness,
43
- ready: false,
44
- startedAt: startedAtIso,
45
- updatedAt: startedAtIso,
46
- lastOutputTail: "",
47
- status: "starting",
48
- process: child,
49
- detached
50
- };
51
- rememberManagedService(input.context, serviceRecord);
52
- const writeLog = (stream, text2) => {
53
- const sanitized = input.sanitizeText(text2);
54
- lastOutput = `${lastOutput}${sanitized}`.slice(-2e3);
55
- updateManagedService(serviceRecord, {
56
- lastOutputTail: lastOutput
57
- });
58
- if (!logClosed) {
59
- log.write(`[${stream}] ${sanitized}`);
60
- input.onProgress?.(stream, text2);
61
- }
62
- };
63
- log.write(`$ ${input.sanitizeText(input.logCommand)}
64
- `);
65
- log.write(`service cwd ${input.cwd}
66
- `);
67
- child.stdout?.on("data", (chunk) => writeLog("stdout", chunk.toString("utf8")));
68
- child.stderr?.on("data", (chunk) => writeLog("stderr", chunk.toString("utf8")));
69
- child.on("close", (code, signal) => {
70
- closed = true;
71
- exitCode = code;
72
- signalCode = signal;
73
- updateManagedService(serviceRecord, {
74
- ready: false,
75
- status: serviceRecord.status === "stopped" ? "stopped" : "exited",
76
- exitCode: code,
77
- signalCode: signal
78
- });
79
- if (!logClosed) {
80
- log.write(`
81
- service process closed exitCode=${code ?? "null"} signal=${signal ?? "null"}
82
- `);
83
- }
84
- });
85
- child.on("error", (error) => {
86
- closed = true;
87
- exitCode = 1;
88
- updateManagedService(serviceRecord, { ready: false, status: "error", exitCode: 1 });
89
- writeLog("system", `service spawn error: ${error.message}
90
- `);
91
- });
92
- const trackOptions = {
93
- detached,
94
- cwd: input.cwd,
95
- label: `BashService:${input.command.slice(0, 80)}`,
96
- retainAfterExit: true
97
- };
98
- input.trackChildProcess?.(child, trackOptions);
99
- const abort = () => {
100
- writeLog("system", "service start cancelled; terminating process.\n");
101
- child.kill();
102
- };
103
- input.abortSignal?.addEventListener("abort", abort, { once: true });
104
- const ready = await waitForReadiness(input.readiness, () => ({
105
- closed,
106
- exitCode,
107
- signalCode,
108
- lastOutput
109
- }));
110
- input.abortSignal?.removeEventListener("abort", abort);
111
- logClosed = true;
112
- log.end(() => {
113
- logClosed = true;
114
- });
115
- const pid = child.pid;
116
- const alive = isChildAlive(child, closed);
117
- const readinessTarget = formatReadinessTarget(input.readiness);
118
- updateManagedService(serviceRecord, {
119
- ready: ready.ok,
120
- status: ready.ok ? "ready" : alive ? "not_ready" : "exited",
121
- lastProbeAt: (/* @__PURE__ */ new Date()).toISOString()
122
- });
123
- const diagnostics = ready.ok ? [] : [
124
- createServiceReadinessDiagnostic(
125
- ready.evidence,
126
- alive ? "Service process is running but readiness did not pass; inspect the log and retry bounded readiness checks." : "Service process exited before readiness; inspect the log before retrying.",
127
- targetFields
128
- )
129
- ];
130
- const text = [
131
- ready.ok ? "Service started and ready." : "Service readiness failed.",
132
- `serviceId ${serviceId}`,
133
- `pid ${pid ?? "unknown"}`,
134
- `cwd ${input.cwd}`,
135
- `ready ${readinessTarget}`,
136
- `alive ${alive ? "yes" : "no"}`,
137
- `elapsedMs ${Date.now() - startedAt}`,
138
- `log ${input.fullOutputPath}`,
139
- ready.evidence ? `evidence ${ready.evidence}` : ""
140
- ].filter(Boolean).join("\n");
141
- return {
142
- text,
143
- details: [
144
- `fullOutputPath: ${input.fullOutputPath}`,
145
- `command: ${input.sanitizeText(input.logCommand)}`,
146
- `serviceId: ${serviceId}`,
147
- `pid: ${pid ?? "unknown"}`,
148
- `cwd: ${input.cwd}`,
149
- `ready: ${readinessTarget}`,
150
- `alive: ${alive ? "yes" : "no"}`,
151
- `evidence: ${ready.evidence}`
152
- ].join("\n"),
153
- data: {
154
- exitCode: ready.ok ? 0 : 1,
155
- outcome: ready.ok ? "service_ready" : alive ? "service_not_ready" : "service_exited",
156
- service: {
157
- serviceId,
158
- pid,
159
- command: input.logCommand,
160
- cwd: input.cwd,
161
- logPath: input.fullOutputPath,
162
- status: ready.ok ? "ready" : alive ? "not_ready" : "exited",
163
- ready: ready.ok,
164
- target: targetFields.target,
165
- targetHost: targetFields.targetHost,
166
- targetPort: targetFields.targetPort,
167
- alive,
168
- readiness: {
169
- ok: ready.ok,
170
- target: readinessTarget,
171
- elapsedMs: ready.elapsedMs,
172
- evidence: ready.evidence
173
- }
174
- },
175
- ...diagnostics.length > 0 ? { diagnostics } : {}
176
- },
177
- fullOutputPath: input.fullOutputPath
178
- };
179
- }
180
- async function runBashServiceLifecycleAction(action, context) {
181
- if (action.action === "fetch") {
182
- return runServiceFetchCheck(action);
183
- }
184
- const service = findManagedService(context, action.serviceId);
185
- if (!service) {
186
- return serviceActionOutput({
187
- action: action.action,
188
- serviceId: action.serviceId,
189
- outcome: "service_missing",
190
- text: `Service ${action.serviceId} is not registered by Linghun.`,
191
- service: { serviceId: action.serviceId, status: "missing", ready: false }
192
- });
193
- }
194
- if (action.action === "status") {
195
- refreshManagedServiceStatus(service);
196
- return serviceActionOutput({
197
- action: action.action,
198
- serviceId: service.serviceId,
199
- outcome: service.status === "ready" ? "service_ready" : `service_${service.status}`,
200
- text: formatServiceStatusText(service),
201
- service: serializeManagedService(service)
202
- });
203
- }
204
- if (action.action === "probe") {
205
- refreshManagedServiceStatus(service);
206
- const probe = service.status === "exited" || service.status === "stopped" ? { ok: false, evidence: `service ${service.serviceId} is ${service.status}` } : await probeReadiness(service.readiness);
207
- updateManagedService(service, {
208
- ready: probe.ok,
209
- status: probe.ok ? "ready" : service.status === "exited" || service.status === "stopped" ? service.status : "not_ready",
210
- lastProbeAt: (/* @__PURE__ */ new Date()).toISOString()
211
- });
212
- const diagnostics = probe.ok ? [] : [
213
- createServiceReadinessDiagnostic(
214
- probe.evidence,
215
- "Service lifecycle probe did not pass; inspect logs or stop/restart the registered service.",
216
- service
217
- )
218
- ];
219
- return serviceActionOutput({
220
- action: action.action,
221
- serviceId: service.serviceId,
222
- outcome: probe.ok ? "service_ready" : "service_not_ready",
223
- text: [
224
- `Service probe ${probe.ok ? "ready" : "not-ready"}.`,
225
- `serviceId ${service.serviceId}`,
226
- `target ${service.target ?? formatReadinessTarget(service.readiness)}`,
227
- `evidence ${probe.evidence}`
228
- ].join("\n"),
229
- service: { ...serializeManagedService(service), evidence: probe.evidence },
230
- diagnostics
231
- });
232
- }
233
- if (action.action === "logs") {
234
- const tail = await readServiceTail(service, action.tailBytes ?? DEFAULT_LOG_TAIL_BYTES);
235
- return serviceActionOutput({
236
- action: action.action,
237
- serviceId: service.serviceId,
238
- outcome: "service_logs",
239
- text: [
240
- `Service logs tail.`,
241
- `serviceId ${service.serviceId}`,
242
- `status ${service.status}`,
243
- tail ? `tail
244
- ${tail}` : "tail <empty>"
245
- ].join("\n"),
246
- service: { ...serializeManagedService(service), logTail: tail }
247
- });
248
- }
249
- refreshManagedServiceStatus(service);
250
- if (service.status === "exited" || service.status === "stopped") {
251
- return serviceActionOutput({
252
- action: action.action,
253
- serviceId: service.serviceId,
254
- outcome: `service_${service.status}`,
255
- text: `Service ${service.serviceId} is already ${service.status}.`,
256
- service: serializeManagedService(service)
257
- });
258
- }
259
- const stop = await stopManagedService(service);
260
- refreshManagedServiceStatus(service);
261
- if (!stop.stopped) {
262
- updateManagedService(service, { ready: false, status: "not_ready" });
263
- return serviceActionOutput({
264
- action: action.action,
265
- serviceId: service.serviceId,
266
- outcome: "service_stop_failed",
267
- text: [
268
- "Service stop failed.",
269
- `serviceId ${service.serviceId}`,
270
- `pid ${service.pid ?? "unknown"}`,
271
- `evidence ${stop.evidence}`
272
- ].join("\n"),
273
- service: { ...serializeManagedService(service), evidence: stop.evidence },
274
- diagnostics: [
275
- createServiceReadinessDiagnostic(
276
- stop.evidence,
277
- "Linghun could not confirm that the registered service stopped; inspect the process before retrying.",
278
- service
279
- )
280
- ]
281
- });
282
- }
283
- updateManagedService(service, { ready: false, status: "stopped" });
284
- return serviceActionOutput({
285
- action: action.action,
286
- serviceId: service.serviceId,
287
- outcome: "service_stopped",
288
- text: [
289
- "Service stopped.",
290
- `serviceId ${service.serviceId}`,
291
- `pid ${service.pid ?? "unknown"}`
292
- ].join("\n"),
293
- service: serializeManagedService(service)
294
- });
295
- }
296
- async function runServiceFetchCheck(action) {
297
- const startedAt = Date.now();
298
- const retry = Math.max(0, Math.min(action.retry ?? 0, 10));
299
- const intervalMs = Math.max(0, Math.min(action.intervalMs ?? DEFAULT_READY_INTERVAL_MS, 5e3));
300
- let result = { ok: false, evidence: `http ${action.url} not attempted`, body: "" };
301
- for (let attempt = 0; attempt <= retry; attempt += 1) {
302
- result = await fetchHttp(action.url, action.timeoutMs ?? CONNECT_ATTEMPT_TIMEOUT_MS);
303
- const statusOk = action.expectStatus === void 0 ? result.status !== void 0 && result.status >= 200 && result.status < 400 : result.status === action.expectStatus;
304
- const requiredBody = normalizeStringList(action.bodyContains);
305
- const missingBody = requiredBody.filter((item) => !result.body.includes(item));
306
- if (result.ok && statusOk && missingBody.length === 0) {
307
- return serviceFetchOutput(action, result, [], Date.now() - startedAt, missingBody);
308
- }
309
- result = {
310
- ...result,
311
- ok: false,
312
- evidence: [
313
- result.evidence,
314
- statusOk ? "" : `expected status ${action.expectStatus ?? "2xx/3xx"}`,
315
- missingBody.length > 0 ? `missing body ${missingBody.join(", ")}` : ""
316
- ].filter(Boolean).join("; ")
317
- };
318
- if (attempt < retry) await delay(intervalMs);
319
- }
320
- const target = readReadinessTargetFields({ type: "http", url: action.url });
321
- return serviceFetchOutput(
322
- action,
323
- result,
324
- [
325
- createServiceReadinessDiagnostic(
326
- result.evidence,
327
- "Explicit service fetch check failed; inspect the registered service or served index before final verification.",
328
- target
329
- )
330
- ],
331
- Date.now() - startedAt,
332
- normalizeStringList(action.bodyContains).filter((item) => !result.body.includes(item))
333
- );
334
- }
335
- function fetchHttp(url, timeoutMs) {
336
- return new Promise((resolve) => {
337
- let parsed;
338
- try {
339
- parsed = new URL(url);
340
- } catch {
341
- resolve({ ok: false, evidence: `invalid fetch url: ${url}`, body: "" });
342
- return;
343
- }
344
- const request = (parsed.protocol === "https:" ? httpsRequest : httpRequest)(
345
- parsed,
346
- { method: "GET", timeout: Math.max(1, timeoutMs) },
347
- (response) => {
348
- const chunks = [];
349
- let size = 0;
350
- response.on("data", (chunk) => {
351
- if (size >= FETCH_BODY_LIMIT_BYTES) return;
352
- const remaining = FETCH_BODY_LIMIT_BYTES - size;
353
- chunks.push(chunk.subarray(0, remaining));
354
- size += Math.min(chunk.length, remaining);
355
- });
356
- response.on("end", () => {
357
- const status = response.statusCode ?? 0;
358
- const body = Buffer.concat(chunks).toString("utf8");
359
- resolve({
360
- ok: status >= 200 && status < 400,
361
- status,
362
- body,
363
- evidence: `http ${url} status ${status}`
364
- });
365
- });
366
- }
367
- );
368
- request.once("timeout", () => {
369
- request.destroy();
370
- resolve({ ok: false, evidence: `http ${url} timed out`, body: "" });
371
- });
372
- request.once("error", (error) => resolve({ ok: false, evidence: `http ${url} failed: ${error.message}`, body: "" }));
373
- request.end();
374
- });
375
- }
376
- function serviceFetchOutput(action, result, diagnostics, elapsedMs, missingBody) {
377
- const target = readReadinessTargetFields({ type: "http", url: action.url });
378
- return {
379
- text: [
380
- `Service fetch ${diagnostics.length === 0 ? "ready" : "not-ready"}.`,
381
- `target ${action.url}`,
382
- `status ${result.status ?? "unknown"}`,
383
- `elapsedMs ${elapsedMs}`,
384
- `evidence ${result.evidence}`,
385
- missingBody.length > 0 ? `missingBody ${missingBody.join(", ")}` : ""
386
- ].filter(Boolean).join("\n"),
387
- data: {
388
- exitCode: diagnostics.length === 0 ? 0 : 1,
389
- outcome: diagnostics.length === 0 ? "service_ready" : "service_not_ready",
390
- service: {
391
- ...target,
392
- ready: diagnostics.length === 0,
393
- status: diagnostics.length === 0 ? "ready" : "not_ready",
394
- fetch: {
395
- status: result.status,
396
- expectedStatus: action.expectStatus,
397
- bodyContains: normalizeStringList(action.bodyContains),
398
- missingBody
399
- }
400
- },
401
- ...diagnostics.length > 0 ? { diagnostics } : {}
402
- }
403
- };
404
- }
405
- async function waitForReadiness(readiness, processState) {
406
- const startedAt = Date.now();
407
- const timeoutMs = readiness.timeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
408
- const intervalMs = readiness.intervalMs ?? DEFAULT_READY_INTERVAL_MS;
409
- const deadline = startedAt + timeoutMs;
410
- let lastEvidence = "";
411
- while (Date.now() <= deadline) {
412
- const state = processState();
413
- if (state.closed) {
414
- return {
415
- ok: false,
416
- evidence: `service process exited before readiness exitCode=${state.exitCode ?? "null"} signal=${state.signalCode ?? "null"} tail=${state.lastOutput.replace(/\s+/gu, " ").trim()}`,
417
- elapsedMs: Date.now() - startedAt
418
- };
419
- }
420
- const probe = readiness.type === "tcp" ? await probeTcp(readiness.host ?? "127.0.0.1", readiness.port) : await probeHttp(readiness.url);
421
- if (probe.ok) {
422
- return { ok: true, evidence: probe.evidence, elapsedMs: Date.now() - startedAt };
423
- }
424
- lastEvidence = probe.evidence;
425
- await delay(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
426
- }
427
- return {
428
- ok: false,
429
- evidence: `readiness timed out after ${timeoutMs}ms: ${lastEvidence || formatReadinessTarget(readiness)}`,
430
- elapsedMs: Date.now() - startedAt
431
- };
432
- }
433
- function probeTcp(host, port) {
434
- return new Promise((resolve) => {
435
- const socket = connectTcp({ host, port });
436
- const finish = (ok, evidence) => {
437
- socket.removeAllListeners();
438
- socket.destroy();
439
- resolve({ ok, evidence });
440
- };
441
- socket.setTimeout(CONNECT_ATTEMPT_TIMEOUT_MS);
442
- socket.once("connect", () => finish(true, `tcp ${host}:${port} accepted connection`));
443
- socket.once("timeout", () => finish(false, `tcp ${host}:${port} connection timed out`));
444
- socket.once("error", (error) => finish(false, `tcp ${host}:${port} failed: ${error.message}`));
445
- });
446
- }
447
- function probeHttp(url) {
448
- return new Promise((resolve) => {
449
- let parsed;
450
- try {
451
- parsed = new URL(url);
452
- } catch {
453
- resolve({ ok: false, evidence: `invalid health url: ${url}` });
454
- return;
455
- }
456
- const request = (parsed.protocol === "https:" ? httpsRequest : httpRequest)(
457
- parsed,
458
- { method: "GET", timeout: CONNECT_ATTEMPT_TIMEOUT_MS },
459
- (response) => {
460
- response.resume();
461
- const status = response.statusCode ?? 0;
462
- resolve({
463
- ok: status >= 200 && status < 400,
464
- evidence: `http ${url} status ${status}`
465
- });
466
- }
467
- );
468
- request.once("timeout", () => {
469
- request.destroy();
470
- resolve({ ok: false, evidence: `http ${url} timed out` });
471
- });
472
- request.once("error", (error) => resolve({ ok: false, evidence: `http ${url} failed: ${error.message}` }));
473
- request.end();
474
- });
475
- }
476
- function probeReadiness(readiness) {
477
- return readiness.type === "tcp" ? probeTcp(readiness.host ?? "127.0.0.1", readiness.port) : probeHttp(readiness.url);
478
- }
479
- function isChildAlive(child, closed) {
480
- return !closed && child.exitCode === null && child.signalCode === null;
481
- }
482
- function createServiceReadinessDiagnostic(evidence, suggestion, target) {
483
- return {
484
- type: "service_readiness",
485
- severity: "recoverable",
486
- evidence,
487
- suggestion,
488
- ...target?.target ? { target: target.target } : {},
489
- ...target?.targetHost ? { targetHost: target.targetHost } : {},
490
- ...target?.targetPort !== void 0 ? { targetPort: target.targetPort } : {}
491
- };
492
- }
493
- function formatReadinessTarget(readiness) {
494
- if (readiness.type === "tcp") {
495
- return `tcp://${readiness.host ?? "127.0.0.1"}:${readiness.port}`;
496
- }
497
- return readiness.url;
498
- }
499
- function readReadinessTargetFields(readiness) {
500
- if (readiness.type === "tcp") {
501
- const host = readiness.host ?? "127.0.0.1";
502
- return {
503
- target: `${host}:${readiness.port}`,
504
- targetHost: host,
505
- targetPort: readiness.port
506
- };
507
- }
508
- try {
509
- const url = new URL(readiness.url);
510
- const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
511
- return {
512
- target: readiness.url,
513
- targetHost: url.hostname,
514
- targetPort: Number.isInteger(port) ? port : void 0
515
- };
516
- } catch {
517
- return { target: readiness.url };
518
- }
519
- }
520
- function rememberManagedService(context, record) {
521
- context.services = [
522
- record,
523
- ...(context.services ?? []).filter((item) => item.serviceId !== record.serviceId)
524
- ].slice(0, MAX_MANAGED_SERVICES);
525
- }
526
- function findManagedService(context, serviceId) {
527
- return (context.services ?? []).find((service) => service.serviceId === serviceId);
528
- }
529
- function updateManagedService(service, patch) {
530
- Object.assign(service, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
531
- }
532
- function refreshManagedServiceStatus(service) {
533
- if (service.status === "stopped") return;
534
- if (service.process && !isChildAlive(service.process, service.status === "exited")) {
535
- updateManagedService(service, {
536
- ready: false,
537
- status: "exited",
538
- exitCode: service.process.exitCode,
539
- signalCode: service.process.signalCode
540
- });
541
- }
542
- }
543
- function serializeManagedService(service) {
544
- const alive = service.process ? isChildAlive(service.process, service.status === "exited") : false;
545
- return {
546
- serviceId: service.serviceId,
547
- pid: service.pid,
548
- cwd: service.cwd,
549
- command: service.command,
550
- logPath: service.logPath,
551
- target: service.target,
552
- targetHost: service.targetHost,
553
- targetPort: service.targetPort,
554
- readiness: {
555
- type: service.readiness.type,
556
- target: formatReadinessTarget(service.readiness)
557
- },
558
- ready: service.ready,
559
- alive,
560
- status: service.status,
561
- startedAt: service.startedAt,
562
- updatedAt: service.updatedAt,
563
- lastProbeAt: service.lastProbeAt,
564
- lastOutputTail: service.lastOutputTail,
565
- exitCode: service.exitCode,
566
- signalCode: service.signalCode
567
- };
568
- }
569
- function formatServiceStatusText(service) {
570
- return [
571
- `Service status ${service.status}.`,
572
- `serviceId ${service.serviceId}`,
573
- `pid ${service.pid ?? "unknown"}`,
574
- `ready ${service.ready ? "yes" : "no"}`,
575
- `target ${service.target ?? formatReadinessTarget(service.readiness)}`,
576
- `log ${service.logPath}`
577
- ].join("\n");
578
- }
579
- function serviceActionOutput(input) {
580
- return {
581
- text: input.text,
582
- details: input.text,
583
- data: {
584
- exitCode: input.diagnostics && input.diagnostics.length > 0 ? 1 : 0,
585
- outcome: input.outcome,
586
- service: input.service,
587
- ...input.diagnostics && input.diagnostics.length > 0 ? { diagnostics: input.diagnostics } : {}
588
- }
589
- };
590
- }
591
- async function readServiceTail(service, tailBytes) {
592
- const boundedBytes = Math.max(1, Math.min(tailBytes, 16e3));
593
- let diskTail = "";
594
- try {
595
- const text = await readFile(service.logPath, "utf8");
596
- diskTail = text.slice(-boundedBytes);
597
- } catch {
598
- diskTail = "";
599
- }
600
- const combined = `${diskTail}${service.lastOutputTail ? `
601
- ${service.lastOutputTail}` : ""}`;
602
- return combined.replace(/\0/g, "").slice(-boundedBytes);
603
- }
604
- async function stopManagedService(service) {
605
- if (process.platform === "win32" && service.pid) {
606
- await taskkillWindowsPid(service.pid);
607
- const stopped = await waitForServiceExit(service, STOP_KILL_WAIT_MS);
608
- return {
609
- stopped,
610
- evidence: stopped ? `taskkill confirmed service ${service.serviceId} stopped` : `taskkill did not confirm service ${service.serviceId} stopped`
611
- };
612
- }
613
- if (service.detached && service.pid) {
614
- try {
615
- process.kill(-service.pid, "SIGTERM");
616
- if (await waitForServiceExit(service, STOP_TERM_WAIT_MS)) {
617
- return { stopped: true, evidence: `SIGTERM process group ${service.pid} confirmed stopped` };
618
- }
619
- } catch {
620
- }
621
- } else {
622
- service.process?.kill("SIGTERM");
623
- if (await waitForServiceExit(service, STOP_TERM_WAIT_MS)) {
624
- return { stopped: true, evidence: `SIGTERM pid ${service.pid ?? "unknown"} confirmed stopped` };
625
- }
626
- }
627
- if (service.detached && service.pid) {
628
- try {
629
- process.kill(-service.pid, "SIGKILL");
630
- } catch {
631
- service.process?.kill("SIGKILL");
632
- }
633
- } else {
634
- service.process?.kill("SIGKILL");
635
- }
636
- if (await waitForServiceExit(service, STOP_KILL_WAIT_MS)) {
637
- return { stopped: true, evidence: `SIGKILL pid ${service.pid ?? "unknown"} confirmed stopped` };
638
- }
639
- return { stopped: false, evidence: `service ${service.serviceId} still appears alive after SIGTERM/SIGKILL` };
640
- }
641
- async function waitForServiceExit(service, timeoutMs) {
642
- const deadline = Date.now() + timeoutMs;
643
- while (Date.now() <= deadline) {
644
- refreshManagedServiceStatus(service);
645
- if (service.status === "exited" || service.status === "stopped") return true;
646
- if (!service.process || !isChildAlive(service.process, false)) return true;
647
- await delay(50);
648
- }
649
- refreshManagedServiceStatus(service);
650
- return service.status === "exited" || service.status === "stopped" || !service.process || !isChildAlive(service.process, false);
651
- }
652
- function taskkillWindowsPid(pid) {
653
- return new Promise((resolve) => {
654
- const killer = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { windowsHide: true });
655
- const timeout = setTimeout(() => {
656
- killer.kill("SIGKILL");
657
- resolve();
658
- }, 1e3);
659
- const finish = () => {
660
- clearTimeout(timeout);
661
- resolve();
662
- };
663
- killer.once("error", finish);
664
- killer.once("close", finish);
665
- });
666
- }
667
- function delay(ms) {
668
- return new Promise((resolve) => setTimeout(resolve, ms));
669
- }
670
- function normalizeStringList(value) {
671
- if (value === void 0) return [];
672
- return Array.isArray(value) ? value : [value];
673
- }
674
-
675
- export {
676
- startBashService,
677
- runBashServiceLifecycleAction
678
- };