livedesk 0.1.444 → 0.1.445

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.
@@ -11,7 +11,13 @@ import net from 'node:net';
11
11
  import os from 'node:os';
12
12
  import { formatClientVersionBanner, readClientPackageVersion } from './client-version.js';
13
13
  import { createClientRuntimeServer } from '../src/runtime/client-runtime-server.js';
14
- import { installAgentTerminationHandlers } from '../src/runtime/agent-process-lifecycle.js';
14
+ import {
15
+ createWindowsProcessTreeTracker,
16
+ drainOwnedWindowsAgentTree,
17
+ drainOwnedUnixAgentTree,
18
+ installAgentTerminationHandlers,
19
+ signalAgentTree
20
+ } from '../src/runtime/agent-process-lifecycle.js';
15
21
  import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
16
22
  import {
17
23
  inspectLinuxVideoAcceleration,
@@ -67,6 +73,22 @@ let networkChangeMonitor = null;
67
73
  const sessionRefreshesInFlight = new WeakMap();
68
74
  installAgentTerminationHandlers({ getAgentProcess: () => activeAgentProcess });
69
75
 
76
+ function requestActiveAgentStop(signal = 'SIGTERM') {
77
+ const child = activeAgentProcess;
78
+ if (!child) return false;
79
+ if (process.platform === 'win32') {
80
+ // Internal reconnect/slot/role restarts use the same immutable
81
+ // PID+CreationDate tree contract as unexpected exit and terminal
82
+ // shutdown. Store the shared proof promise so no replacement can race.
83
+ if (!child.livedeskTreeStopPromise) {
84
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
85
+ }
86
+ return true;
87
+ }
88
+ signalAgentTree(child, signal);
89
+ return true;
90
+ }
91
+
70
92
  export function requestLocalDiscoveryWake(reason = 'local-trigger') {
71
93
  const previous = discoveryWakeController;
72
94
  discoveryWakeController = new AbortController();
@@ -2783,7 +2805,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2783
2805
  res.end(JSON.stringify({ ok: true, restarting: true, role: 'hub', roleVersion: nextRoleVersion }));
2784
2806
  setTimeout(() => {
2785
2807
  try {
2786
- activeAgentProcess?.kill();
2808
+ requestActiveAgentStop();
2787
2809
  } catch {
2788
2810
  }
2789
2811
  try {
@@ -3080,7 +3102,7 @@ async function chooseClientConnection(supabase, options = {}) {
3080
3102
  writeLocalRoleCache(role, options.deviceId, roleVersion);
3081
3103
  roleRestartRequest = { role, requestedAt: new Date().toISOString() };
3082
3104
  requestLocalDiscoveryWake('role-change');
3083
- try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop observes the role request */ }
3105
+ try { requestActiveAgentStop(); } catch { /* the lifecycle loop observes the role request */ }
3084
3106
  return { ok: true, restarting: true, role, roleVersion };
3085
3107
  },
3086
3108
  videoAcceleration: linuxVideoAccelerationStatus,
@@ -3098,7 +3120,7 @@ async function chooseClientConnection(supabase, options = {}) {
3098
3120
  requestedAt: new Date().toISOString()
3099
3121
  };
3100
3122
  requestLocalDiscoveryWake('linux-video-acceleration-ready');
3101
- try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop restarts with the new ffmpeg preference */ }
3123
+ try { requestActiveAgentStop(); } catch { /* the lifecycle loop restarts with the new ffmpeg preference */ }
3102
3124
  },
3103
3125
  assignSlot: async request => {
3104
3126
  const result = await requestHubSlotAssignment(request);
@@ -3113,20 +3135,20 @@ async function chooseClientConnection(supabase, options = {}) {
3113
3135
  };
3114
3136
  requestLocalDiscoveryWake('slot-updated');
3115
3137
  setTimeout(() => {
3116
- try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop restarts with the saved slot */ }
3138
+ try { requestActiveAgentStop(); } catch { /* the lifecycle loop restarts with the saved slot */ }
3117
3139
  }, 150);
3118
3140
  return { ...result, slotNumber, saved: true, restarting: true };
3119
3141
  },
3120
3142
  onRestart: () => process.kill(process.pid, 'SIGTERM'),
3121
3143
  onShutdown: () => process.kill(process.pid, 'SIGTERM'),
3122
- onDisconnect: () => activeAgentProcess?.kill(),
3144
+ onDisconnect: () => requestActiveAgentStop(),
3123
3145
  // Ending the current Agent causes the unified lifecycle loop to
3124
3146
  // discard its old manager/pair token and start Supabase discovery
3125
3147
  // again. Leaving this empty made the Reconnect button a success
3126
3148
  // response with no actual transport work.
3127
3149
  onReconnect: () => {
3128
3150
  requestLocalDiscoveryWake('manual-reconnect');
3129
- activeAgentProcess?.kill();
3151
+ requestActiveAgentStop();
3130
3152
  }
3131
3153
  });
3132
3154
  await connectionPage.start();
@@ -4103,6 +4125,7 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4103
4125
  restartVerified: false
4104
4126
  });
4105
4127
  const ownsProcessGroup = process.platform !== 'win32';
4128
+ const agentSpawnedAtMs = Date.now();
4106
4129
  const child = spawn(command, args, {
4107
4130
  env,
4108
4131
  stdio: 'inherit',
@@ -4113,24 +4136,78 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4113
4136
  windowsHide: true
4114
4137
  });
4115
4138
  child.livedeskOwnsProcessGroup = ownsProcessGroup;
4139
+ child.livedeskWindowsTreeTracker = createWindowsProcessTreeTracker(child, {
4140
+ spawnedAtMs: agentSpawnedAtMs
4141
+ });
4116
4142
  activeAgentProcess = child;
4117
4143
  if (typeof onStart === 'function') {
4118
4144
  onStart(child);
4119
4145
  }
4120
4146
 
4121
- return new Promise(resolve => {
4122
- child.once('error', error => {
4123
- console.error(`Failed to launch ${command}: ${error.message}`);
4124
- resolve({ code: 1, signal: null });
4125
- });
4126
- child.once('exit', (code, signal) => {
4127
- if (activeAgentProcess === child) {
4128
- activeAgentProcess = null;
4129
- }
4130
- resolve({ code: code ?? 0, signal });
4131
- });
4132
- });
4133
- }
4147
+ return new Promise(resolve => {
4148
+ let settling = false;
4149
+ child.once('error', error => {
4150
+ if (settling) return;
4151
+ settling = true;
4152
+ child.livedeskWindowsTreeTracker?.stop?.();
4153
+ if (activeAgentProcess === child) {
4154
+ activeAgentProcess = null;
4155
+ }
4156
+ console.error(`Failed to launch ${command}: ${error.message}`);
4157
+ resolve({ code: 1, signal: null });
4158
+ });
4159
+ child.once('exit', (code, signal) => {
4160
+ if (settling) return;
4161
+ settling = true;
4162
+ child.livedeskWindowsTreeTracker?.markRootExited?.();
4163
+ // The Unix Agent is a dedicated process-group leader. Its exit
4164
+ // event proves only that the leader stopped; SCK/FFmpeg capture
4165
+ // descendants can still own that PGID. Keep active ownership and
4166
+ // do not let the lifecycle loop launch a replacement until the
4167
+ // complete group has drained.
4168
+ void (async () => {
4169
+ try {
4170
+ if (process.platform === 'win32') {
4171
+ if (!child.livedeskTreeStopPromise) {
4172
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
4173
+ }
4174
+ const windowsTreeStop = await child.livedeskTreeStopPromise;
4175
+ if (windowsTreeStop?.ok === false) {
4176
+ throw new Error(
4177
+ `exact Windows Agent tree drain failed: `
4178
+ + `${windowsTreeStop.error?.message || 'bounded immutable-tree cleanup failed'}`
4179
+ );
4180
+ }
4181
+ }
4182
+ await drainOwnedUnixAgentTree(child);
4183
+ } catch (error) {
4184
+ console.error(
4185
+ `[LiveDesk Client] Could not drain exited Agent tree pid=${child.pid || 'unknown'}: `
4186
+ + `${error?.message || error}`
4187
+ );
4188
+ // Block this lifecycle iteration from spawning a
4189
+ // replacement, but settle explicitly so the launcher
4190
+ // terminates instead of hanging forever.
4191
+ child.livedeskWindowsTreeTracker?.stop?.();
4192
+ if (activeAgentProcess === child) {
4193
+ activeAgentProcess = null;
4194
+ }
4195
+ resolve({
4196
+ code: 1,
4197
+ signal: null,
4198
+ lifecycleFailure: String(error?.message || error)
4199
+ });
4200
+ return;
4201
+ }
4202
+ child.livedeskWindowsTreeTracker?.stop?.();
4203
+ if (activeAgentProcess === child) {
4204
+ activeAgentProcess = null;
4205
+ }
4206
+ resolve({ code: code ?? 0, signal });
4207
+ })();
4208
+ });
4209
+ });
4210
+ }
4134
4211
 
4135
4212
  function shouldUseFast(parsed) {
4136
4213
  if (parsed.engine === 'node') {
@@ -4286,9 +4363,36 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4286
4363
  pid: child.pid,
4287
4364
  startedAt: new Date().toISOString(),
4288
4365
  state: 'running'
4289
- });
4290
- });
4291
- }
4366
+ });
4367
+ });
4368
+ }
4369
+ // Contract: once the owned Agent exits, the local runtime must stop
4370
+ // advertising its old PID before any restart or Hub rediscovery.
4371
+ // Keeping a dead PID makes status diagnostics lie and causes manual
4372
+ // reconnect to target a process that no longer exists.
4373
+ prepared.connectionPage?.update({
4374
+ agent: {
4375
+ requestedEngine: prepared.engine,
4376
+ state: 'stopped',
4377
+ pid: 0,
4378
+ stoppedAt: new Date().toISOString(),
4379
+ exitCode: Number.isInteger(result?.code) ? result.code : null,
4380
+ exitSignal: result?.signal || ''
4381
+ }
4382
+ });
4383
+ if (result?.lifecycleFailure) {
4384
+ prepared.connectionPage?.update({
4385
+ agent: {
4386
+ requestedEngine: prepared.engine,
4387
+ state: 'failed',
4388
+ pid: 0,
4389
+ error: result.lifecycleFailure
4390
+ },
4391
+ message: 'LiveDesk stopped because the previous Windows capture tree could not be drained safely.'
4392
+ });
4393
+ connectionPage?.close?.();
4394
+ throw new Error(result.lifecycleFailure);
4395
+ }
4292
4396
  const requestedRoleRestart = connectionPage?.getRoleRestartRequest?.();
4293
4397
  if (requestedRoleRestart?.role) {
4294
4398
  if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
@@ -4302,7 +4406,8 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4302
4406
  connectionPage?.update({
4303
4407
  agent: {
4304
4408
  requestedEngine: prepared.engine,
4305
- state: 'reconnecting'
4409
+ state: 'reconnecting',
4410
+ pid: 0
4306
4411
  },
4307
4412
  message: restart.reason === 'slot-updated'
4308
4413
  ? `Slot ${String(restart.slotNumber || prepared.slot || '').padStart(3, '0')} saved. Restarting the LiveDesk Agent.`
@@ -4331,10 +4436,11 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4331
4436
  ? 'LiveDesk Hub pair token changed. Refreshing Hub discovery now.'
4332
4437
  : 'LiveDesk Hub connection ended. Looking for the current Hub now.');
4333
4438
  connectionPage?.update({
4334
- agent: {
4335
- requestedEngine: prepared.engine,
4336
- state: 'reconnecting'
4337
- },
4439
+ agent: {
4440
+ requestedEngine: prepared.engine,
4441
+ state: 'reconnecting',
4442
+ pid: 0
4443
+ },
4338
4444
  manager: '',
4339
4445
  message: 'The previous Hub is unavailable. LiveDesk is finding the current Hub automatically.'
4340
4446
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.199",
3
+ "version": "0.1.200",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@livedesk/fast-linux-x64": "0.1.406",
45
- "@livedesk/fast-osx-arm64": "0.1.406",
46
- "@livedesk/fast-osx-x64": "0.1.406",
47
- "@livedesk/fast-win-x64": "0.1.406"
44
+ "@livedesk/fast-linux-x64": "0.1.407",
45
+ "@livedesk/fast-osx-arm64": "0.1.407",
46
+ "@livedesk/fast-osx-x64": "0.1.407",
47
+ "@livedesk/fast-win-x64": "0.1.407"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"