@pushpalsdev/cli 1.1.39 → 1.1.40

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.
@@ -58,11 +58,13 @@ function shouldRestartService(attempts, maxAttempts = DEFAULT_SERVICE_MANAGER_MA
58
58
  return normalizedAttempts < normalizedMax;
59
59
  }
60
60
  function pipeProcessStreamToLines(stream, onLine) {
61
- if (!stream || typeof stream === "number" || typeof stream.getReader !== "function")
62
- return;
61
+ if (!stream || typeof stream === "number" || typeof stream.getReader !== "function") {
62
+ return { cancel: () => {} };
63
+ }
63
64
  const reader = stream.getReader();
64
65
  const decoder = new TextDecoder;
65
66
  let pending = "";
67
+ let cancelled = false;
66
68
  (async () => {
67
69
  try {
68
70
  while (true) {
@@ -86,9 +88,21 @@ function pipeProcessStreamToLines(stream, onLine) {
86
88
  if (tail)
87
89
  onLine?.(tail);
88
90
  } catch {} finally {
89
- reader.releaseLock();
91
+ try {
92
+ reader.releaseLock();
93
+ } catch {}
90
94
  }
91
95
  })();
96
+ return {
97
+ cancel: () => {
98
+ if (cancelled)
99
+ return;
100
+ cancelled = true;
101
+ try {
102
+ reader.cancel().catch(() => {});
103
+ } catch {}
104
+ }
105
+ };
92
106
  }
93
107
  function spawnManagedService(spec) {
94
108
  const env = { ...spec.env ?? {} };
@@ -98,8 +112,8 @@ function spawnManagedService(spec) {
98
112
  stdout: "pipe",
99
113
  stderr: "pipe"
100
114
  });
101
- pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
102
- pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
115
+ const stdoutPipe = pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
116
+ const stderrPipe = pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
103
117
  const service = {
104
118
  name: spec.name,
105
119
  proc,
@@ -109,7 +123,11 @@ function spawnManagedService(spec) {
109
123
  exited: false,
110
124
  exitCode: null,
111
125
  launchedAtMs: Date.now(),
112
- logPath: spec.logPath
126
+ logPath: spec.logPath,
127
+ stopOutputPipes: () => {
128
+ stdoutPipe.cancel();
129
+ stderrPipe.cancel();
130
+ }
113
131
  };
114
132
  proc.exited.then((code) => {
115
133
  service.exited = true;
@@ -161,6 +179,7 @@ class ServiceManager {
161
179
  const existing = this.services.get(spec.name);
162
180
  if (existing && !existing.exited) {
163
181
  try {
182
+ existing.stopOutputPipes?.();
164
183
  const pid = existing.proc.pid;
165
184
  if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
166
185
  Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
@@ -224,6 +243,7 @@ class ServiceManager {
224
243
  this.stopped = true;
225
244
  for (const service of this.services.values()) {
226
245
  try {
246
+ service.stopOutputPipes?.();
227
247
  const pid = service.proc.pid;
228
248
  if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
229
249
  Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
@@ -3221,6 +3241,7 @@ function buildServiceStopCommand(pid, platform = process.platform) {
3221
3241
  function stopRuntimeServices(services) {
3222
3242
  for (const service of services) {
3223
3243
  try {
3244
+ service.stopOutputPipes?.();
3224
3245
  const stopCommand = buildServiceStopCommand(service.proc.pid, process.platform);
3225
3246
  if (stopCommand) {
3226
3247
  Bun.spawnSync(stopCommand, {
@@ -3236,6 +3257,52 @@ function stopRuntimeServices(services) {
3236
3257
  } catch {}
3237
3258
  }
3238
3259
  }
3260
+ async function runWindowsServiceStopCommand(command, timeoutMs) {
3261
+ const proc = Bun.spawn(command, {
3262
+ stdin: "ignore",
3263
+ stdout: "ignore",
3264
+ stderr: "ignore"
3265
+ });
3266
+ let timeout = null;
3267
+ let timedOut = false;
3268
+ const exitCode = await Promise.race([
3269
+ proc.exited,
3270
+ new Promise((resolveTimeout) => {
3271
+ timeout = setTimeout(() => {
3272
+ timedOut = true;
3273
+ try {
3274
+ proc.kill("SIGKILL");
3275
+ } catch {}
3276
+ resolveTimeout(-1);
3277
+ }, Math.max(250, timeoutMs));
3278
+ })
3279
+ ]);
3280
+ if (timeout) {
3281
+ clearTimeout(timeout);
3282
+ timeout = null;
3283
+ }
3284
+ if (timedOut) {
3285
+ await Promise.race([proc.exited.catch(() => -1), Bun.sleep(250)]);
3286
+ return false;
3287
+ }
3288
+ return exitCode === 0;
3289
+ }
3290
+ async function stopRuntimeServicesOnWindows(services, timeoutMs) {
3291
+ const deadline = Date.now() + Math.max(1000, timeoutMs);
3292
+ for (const service of services) {
3293
+ service.stopOutputPipes?.();
3294
+ const stopCommand = buildServiceStopCommand(service.proc.pid, "win32");
3295
+ if (stopCommand) {
3296
+ const remainingMs = Math.max(250, deadline - Date.now());
3297
+ const stopped = await runWindowsServiceStopCommand(stopCommand, Math.min(WINDOWS_TASKKILL_TIMEOUT_MS2, remainingMs));
3298
+ if (stopped)
3299
+ continue;
3300
+ }
3301
+ try {
3302
+ service.proc.kill("SIGKILL");
3303
+ } catch {}
3304
+ }
3305
+ }
3239
3306
  function resolveGracefulShutdownPriority(name) {
3240
3307
  if (name === "source_control_manager")
3241
3308
  return 0;
@@ -3264,7 +3331,7 @@ async function stopRuntimeServicesGracefully(services, timeoutMs = 1e4) {
3264
3331
  return;
3265
3332
  const ordered = [...running].sort((a, b) => resolveGracefulShutdownPriority(a.name) - resolveGracefulShutdownPriority(b.name));
3266
3333
  if (process.platform === "win32") {
3267
- stopRuntimeServices(ordered);
3334
+ await stopRuntimeServicesOnWindows(ordered, timeoutMs);
3268
3335
  await waitForRuntimeServicesExit(ordered, Math.min(1000, timeoutMs));
3269
3336
  return;
3270
3337
  }
@@ -3295,6 +3362,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
3295
3362
  reason,
3296
3363
  requestShutdown = requestLocalRuntimeShutdown,
3297
3364
  shutdownAcceptedDelayMs = 1500,
3365
+ serviceStopTimeoutMs = 1e4,
3298
3366
  onLog = (line) => console.log(line),
3299
3367
  onWarn = (line) => console.warn(line),
3300
3368
  cleanupTasks = []
@@ -3310,7 +3378,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
3310
3378
  } else if (shutdown.detail) {
3311
3379
  onWarn(`[pushpals] ${shutdown.detail}`);
3312
3380
  }
3313
- await stopRuntimeServicesGracefully(services);
3381
+ await stopRuntimeServicesGracefully(services, serviceStopTimeoutMs);
3314
3382
  for (const task of cleanupTasks) {
3315
3383
  await task();
3316
3384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.39",
3
+ "version": "1.1.40",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {