@linxin666/dsh-client-ui-task-board 0.3.18 → 0.3.19

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/lib/client.js CHANGED
@@ -4636,7 +4636,7 @@ window.__ModuleLoader__.load({
4636
4636
  /** The building package's version, when the bundle carries it. */
4637
4637
  function bakedVersion() {
4638
4638
  try {
4639
- return "0.3.18";
4639
+ return "0.3.19";
4640
4640
  } catch {
4641
4641
  return;
4642
4642
  }
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import z from "schemastery";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createHash, timingSafeEqual } from "node:crypto";
4
- import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { dirname, isAbsolute, join, win32 } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
@@ -1487,6 +1487,7 @@ var HostTaskLedger = class {
1487
1487
  this.file = join(dir, "ledger-v2.json");
1488
1488
  this.lockFile = join(dir, "ledger-v2.lock");
1489
1489
  this.schedulerFile = join(dir, "scheduler-v2.json");
1490
+ this.cleanStaleTemporaryFiles(dir);
1490
1491
  this.lockFd = this.acquireLock();
1491
1492
  try {
1492
1493
  this.document = this.load(dir);
@@ -1499,6 +1500,15 @@ var HostTaskLedger = class {
1499
1500
  throw error;
1500
1501
  }
1501
1502
  }
1503
+ /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
1504
+ cleanStaleTemporaryFiles(dir) {
1505
+ try {
1506
+ const entries = readdirSync(dir);
1507
+ for (const entry of entries) if (entry.includes(".tmp-")) try {
1508
+ unlinkSync(join(dir, entry));
1509
+ } catch {}
1510
+ } catch {}
1511
+ }
1502
1512
  /** Revision + scheduler without any task cloning; feeds the SSE event frame. */
1503
1513
  summary() {
1504
1514
  const { importedSources: _imports, ...scheduler } = this.document.scheduler;
@@ -1636,7 +1646,12 @@ var HostTaskLedger = class {
1636
1646
  ...patch
1637
1647
  };
1638
1648
  if (patch.lastTickAt !== void 0 && Object.keys(patch).every((key) => key === "lastTickAt")) {
1639
- this.writeSchedulerSidecar();
1649
+ try {
1650
+ this.writeSchedulerSidecar();
1651
+ } catch (error) {
1652
+ if (error?.code === "ENOSPC") return;
1653
+ throw error;
1654
+ }
1640
1655
  return;
1641
1656
  }
1642
1657
  this.commit(false);
@@ -2691,6 +2706,7 @@ var TaskBoardHostService = class {
2691
2706
  this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
2692
2707
  this.power = options.power ?? new PowerInhibitor();
2693
2708
  this.now = options.now ?? Date.now;
2709
+ installStreamErrorGuards();
2694
2710
  this.ledger.subscribe(() => {
2695
2711
  this.syncPowerReasons();
2696
2712
  this.emit();
@@ -2843,14 +2859,14 @@ var TaskBoardHostService = class {
2843
2859
  }
2844
2860
  scheduleLaunch(opened) {
2845
2861
  this.launch(opened).catch((error) => {
2846
- console.error("[dsh-task-board] execution launch settlement failed", error);
2862
+ safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
2847
2863
  });
2848
2864
  }
2849
2865
  schedulePoll() {
2850
2866
  if (this.pollInFlight || this.disposed) return;
2851
2867
  this.pollInFlight = true;
2852
2868
  this.pollSessions().catch((error) => {
2853
- console.error("[dsh-task-board] session polling failed", error);
2869
+ safeConsoleError("[dsh-task-board] session polling failed", error);
2854
2870
  }).finally(() => {
2855
2871
  this.pollInFlight = false;
2856
2872
  });
@@ -2859,7 +2875,7 @@ var TaskBoardHostService = class {
2859
2875
  if (this.tickInFlight || this.disposed) return;
2860
2876
  this.tickInFlight = true;
2861
2877
  this.tickSchedule(first).catch((error) => {
2862
- console.error("[dsh-task-board] scheduler tick failed", error);
2878
+ safeConsoleError("[dsh-task-board] scheduler tick failed", error);
2863
2879
  }).finally(() => {
2864
2880
  this.tickInFlight = false;
2865
2881
  });
@@ -2877,6 +2893,25 @@ var TaskBoardHostService = class {
2877
2893
  for (const listener of [...this.listeners]) listener();
2878
2894
  }
2879
2895
  };
2896
+ /**
2897
+ * Install stream error listeners on process.stderr and process.stdout so that
2898
+ * transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
2899
+ * closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
2900
+ */
2901
+ function installStreamErrorGuards() {
2902
+ for (const stream of [process.stderr, process.stdout]) if (stream && typeof stream.on === "function") {
2903
+ if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
2904
+ }
2905
+ }
2906
+ /**
2907
+ * Defensively log to console.error without letting stderr write failures
2908
+ * (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
2909
+ */
2910
+ function safeConsoleError(message, ...args) {
2911
+ try {
2912
+ console.error(message, ...args);
2913
+ } catch {}
2914
+ }
2880
2915
  //#endregion
2881
2916
  //#region src/http.ts
2882
2917
  /** Family-default JSON response headers; callers may append or override. */
@@ -53,6 +53,8 @@ export declare class HostTaskLedger {
53
53
  constructor(dir?: string, now?: () => number, options?: {
54
54
  sessionDefaultPermission?: TaskPermission;
55
55
  });
56
+ /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
57
+ private cleanStaleTemporaryFiles;
56
58
  /** Revision + scheduler without any task cloning; feeds the SSE event frame. */
57
59
  summary(): {
58
60
  revision: number;
@@ -53,3 +53,14 @@ export declare class TaskBoardHostService {
53
53
  private syncPowerReasons;
54
54
  private emit;
55
55
  }
56
+ /**
57
+ * Install stream error listeners on process.stderr and process.stdout so that
58
+ * transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
59
+ * closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
60
+ */
61
+ export declare function installStreamErrorGuards(): void;
62
+ /**
63
+ * Defensively log to console.error without letting stderr write failures
64
+ * (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
65
+ */
66
+ export declare function safeConsoleError(message: string, ...args: unknown[]): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
- "version": "0.3.18",
4
+ "version": "0.3.19",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process'
2
2
  import { createHash } from 'node:crypto'
3
- import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
3
+ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
4
4
  import { dirname, join } from 'node:path'
5
5
  import { dshHome } from './dsh-home.ts'
6
6
  import { isValidCron, nextRunAtMs } from './core/schedule.ts'
@@ -286,6 +286,7 @@ export class HostTaskLedger {
286
286
  this.file = join(dir, 'ledger-v2.json')
287
287
  this.lockFile = join(dir, 'ledger-v2.lock')
288
288
  this.schedulerFile = join(dir, 'scheduler-v2.json')
289
+ this.cleanStaleTemporaryFiles(dir)
289
290
  this.lockFd = this.acquireLock()
290
291
  try {
291
292
  this.document = this.load(dir)
@@ -303,6 +304,24 @@ export class HostTaskLedger {
303
304
  }
304
305
  }
305
306
 
307
+ /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
308
+ private cleanStaleTemporaryFiles(dir: string): void {
309
+ try {
310
+ const entries = readdirSync(dir)
311
+ for (const entry of entries) {
312
+ if (entry.includes('.tmp-')) {
313
+ try {
314
+ unlinkSync(join(dir, entry))
315
+ } catch {
316
+ // Best-effort cleanup
317
+ }
318
+ }
319
+ }
320
+ } catch {
321
+ // Directory may not exist yet or cannot be read
322
+ }
323
+ }
324
+
306
325
  /** Revision + scheduler without any task cloning; feeds the SSE event frame. */
307
326
  summary(): { revision: number; scheduler: TaskBoardSchedulerSnapshot } {
308
327
  const { importedSources: _imports, ...scheduler } = this.document.scheduler
@@ -443,7 +462,16 @@ export class HostTaskLedger {
443
462
  // tiny sidecar instead; any other patch still goes through the full
444
463
  // atomic commit.
445
464
  if (patch.lastTickAt !== undefined && Object.keys(patch).every(key => key === 'lastTickAt')) {
446
- this.writeSchedulerSidecar()
465
+ try {
466
+ this.writeSchedulerSidecar()
467
+ } catch (error) {
468
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOSPC') {
469
+ // Disk is full; sidecar persistence fails, but in-memory heartbeat
470
+ // remains updated. Swallow to prevent unhandled log cascade crashes.
471
+ return
472
+ }
473
+ throw error
474
+ }
447
475
  return
448
476
  }
449
477
  this.commit(false)
@@ -45,6 +45,7 @@ export class TaskBoardHostService {
45
45
  this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry)
46
46
  this.power = options.power ?? new PowerInhibitor()
47
47
  this.now = options.now ?? Date.now
48
+ installStreamErrorGuards()
48
49
  this.ledger.subscribe(() => {
49
50
  this.syncPowerReasons()
50
51
  this.emit()
@@ -214,7 +215,7 @@ export class TaskBoardHostService {
214
215
 
215
216
  private scheduleLaunch(opened: OpenedRun): void {
216
217
  void this.launch(opened).catch(error => {
217
- console.error('[dsh-task-board] execution launch settlement failed', error)
218
+ safeConsoleError('[dsh-task-board] execution launch settlement failed', error)
218
219
  })
219
220
  }
220
221
 
@@ -222,7 +223,7 @@ export class TaskBoardHostService {
222
223
  if (this.pollInFlight || this.disposed) return
223
224
  this.pollInFlight = true
224
225
  void this.pollSessions().catch(error => {
225
- console.error('[dsh-task-board] session polling failed', error)
226
+ safeConsoleError('[dsh-task-board] session polling failed', error)
226
227
  }).finally(() => { this.pollInFlight = false })
227
228
  }
228
229
 
@@ -230,7 +231,7 @@ export class TaskBoardHostService {
230
231
  if (this.tickInFlight || this.disposed) return
231
232
  this.tickInFlight = true
232
233
  void this.tickSchedule(first).catch(error => {
233
- console.error('[dsh-task-board] scheduler tick failed', error)
234
+ safeConsoleError('[dsh-task-board] scheduler tick failed', error)
234
235
  }).finally(() => { this.tickInFlight = false })
235
236
  }
236
237
 
@@ -248,3 +249,33 @@ export class TaskBoardHostService {
248
249
  for (const listener of [...this.listeners]) listener()
249
250
  }
250
251
  }
252
+
253
+ /**
254
+ * Install stream error listeners on process.stderr and process.stdout so that
255
+ * transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
256
+ * closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
257
+ */
258
+ export function installStreamErrorGuards(): void {
259
+ for (const stream of [process.stderr, process.stdout]) {
260
+ if (stream && typeof stream.on === 'function') {
261
+ const hasErrorListener = typeof stream.listenerCount === 'function' && stream.listenerCount('error') > 0
262
+ if (!hasErrorListener) {
263
+ stream.on('error', () => {
264
+ // Swallow write stream errors to keep the host process alive
265
+ })
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Defensively log to console.error without letting stderr write failures
273
+ * (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
274
+ */
275
+ export function safeConsoleError(message: string, ...args: unknown[]): void {
276
+ try {
277
+ console.error(message, ...args)
278
+ } catch {
279
+ // Best-effort stderr write; ignore write errors when stderr stream fails
280
+ }
281
+ }