livedesk 0.1.460 → 0.1.461
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.md +28 -5
- package/bin/livedesk.js +189 -1
- package/bootstrap/runtime-lock.js +6 -0
- package/client/package.json +9 -8
- package/client/src/runtime/windows-owned-process-manifest.js +181 -14
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +214 -26
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +406 -4
- package/diagnostics/livedesk-transport-physical-core.mjs +200 -1
- package/diagnostics/livedesk-transport-physical.mjs +60 -1
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +364 -49
- package/hub/src/server.js +4 -0
- package/hub/src/transport/udp-hub-transport.js +29 -6
- package/package.json +10 -9
- package/scripts/sync-web-dist.js +127 -0
- package/web/dist/assets/index-l4YzRK9d.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-JqqQ5DKN.js +0 -25
package/README.md
CHANGED
|
@@ -11,9 +11,15 @@ The `hub/`, `client/`, `runtime-core/`, and `web/` folders in this package are
|
|
|
11
11
|
generated copies. Edit the source packages and run
|
|
12
12
|
`node packages/livedesk/scripts/sync-web-dist.js` before building or packing
|
|
13
13
|
the product. The published `livedesk` package owns both runtime roles; the
|
|
14
|
-
source-only `@livedesk/client` workspace is not a runtime dependency.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
source-only `@livedesk/client` workspace is not a runtime dependency.
|
|
15
|
+
|
|
16
|
+
LiveDesk's top runtime contract is transmission performance together with
|
|
17
|
+
long-running stability. A faster path is not accepted if it permits duplicate
|
|
18
|
+
capture helpers, stale monitor generations, unbounded frame queues, or an
|
|
19
|
+
unproven Direct/P2P/relay transition. Release builds run the same lifecycle,
|
|
20
|
+
transport recovery, and browser H.264 population gates used by CI.
|
|
21
|
+
|
|
22
|
+
## Unified launch
|
|
17
23
|
|
|
18
24
|
```powershell
|
|
19
25
|
npx -y --prefer-online livedesk@latest
|
|
@@ -90,9 +96,26 @@ npx -y --prefer-online livedesk@latest diagnose mac-physical --report ./mac-resu
|
|
|
90
96
|
Use `diagnose mac-physical --help` to inspect requirements without starting a
|
|
91
97
|
runtime or changing cached role/account state.
|
|
92
98
|
|
|
99
|
+
## Physical transport diagnostic
|
|
100
|
+
|
|
101
|
+
While the local Hub is running and at least one Client is sending a current
|
|
102
|
+
Mode 3 H.264 stream, run:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx -y --prefer-online livedesk@latest diagnose transport-physical
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The diagnostic proves one unchanged stream identity through Direct TCP,
|
|
109
|
+
encrypted UDP P2P, encrypted relay, and Direct recovery. Every phase requires
|
|
110
|
+
an independently decodable SPS/PPS/IDR frame observed by the Hub within a
|
|
111
|
+
bounded deadline. It uses an exact-owner temporary lease, changes no firewall
|
|
112
|
+
or router settings, and writes a redacted report under
|
|
113
|
+
`~/.livedesk/diagnostics`. Use `diagnose transport-physical --help` for
|
|
114
|
+
device, Hub URL, timeout, and report-path options.
|
|
115
|
+
|
|
93
116
|
## Frame modes
|
|
94
|
-
|
|
95
|
-
- Mode 2: independent 320x180 RGB565+LZO tiles for large, stable walls.
|
|
117
|
+
|
|
118
|
+
- Mode 2: independent 320x180 RGB565+LZO tiles for large, stable walls.
|
|
96
119
|
- Mode 3: direct hardware H.264 for focused remote control up to 4K.
|
|
97
120
|
- Mode 4: client Mode 2 inputs composited by an isolated Hub worker into one
|
|
98
121
|
bounded H.264 Atlas stream, then split by UV into the normal browser slots.
|
package/bin/livedesk.js
CHANGED
|
@@ -33,7 +33,11 @@ import {
|
|
|
33
33
|
} from '../bootstrap/client-process-identity.js';
|
|
34
34
|
import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
|
|
35
35
|
import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
consumeDeadWindowsOwnedProcessManifest,
|
|
38
|
+
readDeadWindowsOwnedProcessManifest,
|
|
39
|
+
readWindowsOwnedProcessManifest
|
|
40
|
+
} from '../client/src/runtime/windows-owned-process-manifest.js';
|
|
37
41
|
import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
|
|
38
42
|
|
|
39
43
|
const require = createRequire(import.meta.url);
|
|
@@ -1012,6 +1016,57 @@ async function exactWindowsRecordsStillRunning(records) {
|
|
|
1012
1016
|
return records.filter(record => sameExactWindowsProcess(currentByPid.get(record.pid), record));
|
|
1013
1017
|
}
|
|
1014
1018
|
|
|
1019
|
+
async function exactReclaimedWindowsRecordsStillRunning(records) {
|
|
1020
|
+
const targets = (Array.isArray(records) ? records : [])
|
|
1021
|
+
.map(record => normalizeExactWindowsProcessRecord(record, record?.depth))
|
|
1022
|
+
.filter(Boolean);
|
|
1023
|
+
if (targets.length === 0) return [];
|
|
1024
|
+
const targetPids = [...new Set(targets.map(record => record.pid))];
|
|
1025
|
+
const script = [
|
|
1026
|
+
'$ErrorActionPreference = "Stop";',
|
|
1027
|
+
`$targetPids = @(${targetPids.join(',')});`,
|
|
1028
|
+
'$records = @();',
|
|
1029
|
+
'foreach ($targetPidValue in $targetPids) {',
|
|
1030
|
+
' $targetPid = [int]$targetPidValue;',
|
|
1031
|
+
' $target = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,ParentProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1;',
|
|
1032
|
+
' if (-not $target) { continue };',
|
|
1033
|
+
' $records += [pscustomobject]@{',
|
|
1034
|
+
' ProcessId = [int]$target.ProcessId;',
|
|
1035
|
+
' ParentProcessId = [int]$target.ParentProcessId;',
|
|
1036
|
+
' CreationUtcTicks = if ($target.CreationDate) { [string]$target.CreationDate.ToUniversalTime().Ticks } else { "" }',
|
|
1037
|
+
' };',
|
|
1038
|
+
'}',
|
|
1039
|
+
'[pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 4'
|
|
1040
|
+
].join(' ');
|
|
1041
|
+
const result = await runQuiet(
|
|
1042
|
+
'powershell.exe',
|
|
1043
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
1044
|
+
{ timeout: 10000, killSignal: 'SIGKILL' }
|
|
1045
|
+
);
|
|
1046
|
+
if (!result.ok || !result.stdout) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
'LiveDesk could not revalidate the reclaimed Windows process identities: '
|
|
1049
|
+
+ `${result.stderr || result.error?.message || 'targeted CIM query failed'}`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
let parsed;
|
|
1053
|
+
try {
|
|
1054
|
+
parsed = JSON.parse(result.stdout);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
throw new Error(`LiveDesk could not parse reclaimed Windows process identities: ${error.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
const values = parsed?.Records == null
|
|
1059
|
+
? []
|
|
1060
|
+
: Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records];
|
|
1061
|
+
const currentByPid = new Map(
|
|
1062
|
+
values
|
|
1063
|
+
.map(item => normalizeExactWindowsProcessRecord(item))
|
|
1064
|
+
.filter(Boolean)
|
|
1065
|
+
.map(record => [record.pid, record])
|
|
1066
|
+
);
|
|
1067
|
+
return targets.filter(record => sameExactWindowsProcess(currentByPid.get(record.pid), record));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1015
1070
|
async function waitForExactWindowsRecordsExit(records, timeoutMs = 2500) {
|
|
1016
1071
|
const deadline = Date.now() + timeoutMs;
|
|
1017
1072
|
let remaining = records;
|
|
@@ -1061,6 +1116,124 @@ async function terminateExactWindowsProcesses(recordOrRecords) {
|
|
|
1061
1116
|
);
|
|
1062
1117
|
}
|
|
1063
1118
|
|
|
1119
|
+
function normalizeReclaimedWindowsClientOwner(value) {
|
|
1120
|
+
const pid = Number(value?.pid || 0);
|
|
1121
|
+
const ownerToken = String(value?.ownerToken || '').trim();
|
|
1122
|
+
const ownerInstanceMarker = String(value?.ownerInstanceMarker || '').trim();
|
|
1123
|
+
const ownerStartOrder = String(value?.ownerStartOrder || '').trim();
|
|
1124
|
+
if (String(value?.role || '').trim().toLowerCase() !== 'client'
|
|
1125
|
+
|| !Number.isInteger(pid)
|
|
1126
|
+
|| pid <= 1
|
|
1127
|
+
|| !/^[a-f0-9]{32}$/i.test(ownerToken)
|
|
1128
|
+
|| !ownerInstanceMarker.startsWith(`${pid}:`)
|
|
1129
|
+
|| !/^\d+$/.test(ownerStartOrder)) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
return { pid, ownerToken, ownerInstanceMarker, ownerStartOrder };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function observeExactWindowsRecordsEmptyTwice(records) {
|
|
1136
|
+
let emptyObservations = 0;
|
|
1137
|
+
while (emptyObservations < 2) {
|
|
1138
|
+
const remaining = await exactReclaimedWindowsRecordsStillRunning(records);
|
|
1139
|
+
if (remaining.length > 0) {
|
|
1140
|
+
return { confirmed: false, remaining, emptyObservations };
|
|
1141
|
+
}
|
|
1142
|
+
emptyObservations += 1;
|
|
1143
|
+
if (emptyObservations < 2) {
|
|
1144
|
+
await waitMilliseconds(PORT_CLEANUP_POLL_MS);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return { confirmed: true, remaining: [], emptyObservations };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async function drainReclaimedWindowsClientManifest(reclaimedOwner, {
|
|
1151
|
+
maxAttempts = 4
|
|
1152
|
+
} = {}) {
|
|
1153
|
+
if (os.platform() !== 'win32') return { handled: false, reason: 'not-windows' };
|
|
1154
|
+
const exactOwner = normalizeReclaimedWindowsClientOwner(reclaimedOwner);
|
|
1155
|
+
if (!exactOwner) {
|
|
1156
|
+
return { handled: false, reason: 'legacy-or-incomplete-owner' };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const inheritedManifest = readDeadWindowsOwnedProcessManifest({
|
|
1160
|
+
stateDir: MANAGER_STATE_DIR,
|
|
1161
|
+
owner: exactOwner
|
|
1162
|
+
});
|
|
1163
|
+
if (!inheritedManifest.ok) {
|
|
1164
|
+
if (inheritedManifest.missing === true) {
|
|
1165
|
+
if (process.env.LIVEDESK_DEBUG === '1') {
|
|
1166
|
+
console.log(
|
|
1167
|
+
`[LiveDesk] Reclaimed Windows Client generation ${exactOwner.pid} `
|
|
1168
|
+
+ 'has no exact process manifest; continuing legacy cleanup.'
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return { handled: false, reason: 'manifest-missing' };
|
|
1172
|
+
}
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
'The reclaimed Windows Client process manifest was preserved because '
|
|
1175
|
+
+ `its exact owner, timestamp, or records could not be verified: ${
|
|
1176
|
+
inheritedManifest.error?.message || 'manifest validation failed'
|
|
1177
|
+
}`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const tracked = inheritedManifest.records
|
|
1182
|
+
.map(record => normalizeExactWindowsProcessRecord(record, record?.depth))
|
|
1183
|
+
.filter(Boolean);
|
|
1184
|
+
if (tracked.some(record => record.pid === process.pid)) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
'The reclaimed Windows Client process manifest unexpectedly named the '
|
|
1187
|
+
+ 'new launcher; every recorded process was preserved.'
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const attempts = Math.max(1, Number(maxAttempts) || 4);
|
|
1192
|
+
let lastTerminationError = null;
|
|
1193
|
+
for (let attempt = 0; attempt <= attempts; attempt += 1) {
|
|
1194
|
+
const observation = await observeExactWindowsRecordsEmptyTwice(tracked);
|
|
1195
|
+
if (observation.confirmed) {
|
|
1196
|
+
const consumedManifest = consumeDeadWindowsOwnedProcessManifest({
|
|
1197
|
+
stateDir: MANAGER_STATE_DIR,
|
|
1198
|
+
owner: exactOwner,
|
|
1199
|
+
revision: inheritedManifest.revision
|
|
1200
|
+
});
|
|
1201
|
+
if (!consumedManifest.ok) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
'The reclaimed Windows Client process manifest was preserved after '
|
|
1204
|
+
+ `drain because its exact revision could not be consumed safely: ${
|
|
1205
|
+
consumedManifest.error?.message || 'manifest consumption failed'
|
|
1206
|
+
}`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
if (process.env.LIVEDESK_DEBUG === '1') {
|
|
1210
|
+
console.log(
|
|
1211
|
+
`[LiveDesk] Reclaimed Windows Client generation ${exactOwner.pid} `
|
|
1212
|
+
+ `drained ${tracked.length} exact process record(s) and consumed its manifest.`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
return { handled: true, records: tracked, consumedManifest };
|
|
1216
|
+
}
|
|
1217
|
+
if (attempt === attempts) break;
|
|
1218
|
+
|
|
1219
|
+
const descendantFirst = [...observation.remaining].sort((left, right) => (
|
|
1220
|
+
Number(right.depth || 0) - Number(left.depth || 0)
|
|
1221
|
+
));
|
|
1222
|
+
const result = await terminateExactWindowsProcesses(descendantFirst);
|
|
1223
|
+
if (!result.ok) {
|
|
1224
|
+
lastTerminationError = result.error || new Error(result.stderr || 'termination failed');
|
|
1225
|
+
}
|
|
1226
|
+
await waitMilliseconds(PORT_CLEANUP_POLL_MS);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const finalRemaining = await exactReclaimedWindowsRecordsStillRunning(tracked);
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
'The reclaimed Windows Client generation retained exact pid(s): '
|
|
1232
|
+
+ `${finalRemaining.map(record => record.pid).join(', ') || 'unknown'}`
|
|
1233
|
+
+ `${lastTerminationError?.message ? ` (${lastTerminationError.message})` : ''}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1064
1237
|
async function terminateExactWindowsProcessTree(rootDetails, {
|
|
1065
1238
|
label = 'LiveDesk process tree',
|
|
1066
1239
|
maxAttempts = 4,
|
|
@@ -3095,6 +3268,21 @@ async function main() {
|
|
|
3095
3268
|
}
|
|
3096
3269
|
|
|
3097
3270
|
if (resolvedRole.role === 'client') {
|
|
3271
|
+
// A launcher and Agent can both die while an independently spawned FFmpeg
|
|
3272
|
+
// survives. Before any new Client process starts, consume only the exact
|
|
3273
|
+
// manifest generation returned by the stale-lock compare-and-swap. No
|
|
3274
|
+
// process-name scan participates in this recovery path.
|
|
3275
|
+
const reclaimedManifestDrain = await drainReclaimedWindowsClientManifest(lock.reclaimedOwner);
|
|
3276
|
+
const testHoldAfterDrainMs = Number(
|
|
3277
|
+
process.env.LIVEDESK_TEST_HOLD_AFTER_RECLAIMED_WINDOWS_DRAIN_MS || 0
|
|
3278
|
+
);
|
|
3279
|
+
if (reclaimedManifestDrain.handled
|
|
3280
|
+
&& process.env.LIVEDESK_DEBUG === '1'
|
|
3281
|
+
&& Number.isFinite(testHoldAfterDrainMs)
|
|
3282
|
+
&& testHoldAfterDrainMs > 0) {
|
|
3283
|
+
console.log('[LiveDesk test] Holding after reclaimed manifest drain before Client startup.');
|
|
3284
|
+
await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
|
|
3285
|
+
}
|
|
3098
3286
|
const clientRuntimePort = normalizePort(
|
|
3099
3287
|
process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_LOCAL_RUNTIME_PORT,
|
|
3100
3288
|
DEFAULT_MANAGER_HTTP_PORT
|
|
@@ -300,6 +300,7 @@ export function acquireRuntimeLock({
|
|
|
300
300
|
acquired: true,
|
|
301
301
|
path,
|
|
302
302
|
payload,
|
|
303
|
+
reclaimedOwner: null,
|
|
303
304
|
release,
|
|
304
305
|
updateRole: nextRole => updateOwnedLockRole(path, payload, nextRole)
|
|
305
306
|
};
|
|
@@ -352,10 +353,15 @@ export function acquireRuntimeLock({
|
|
|
352
353
|
released = true;
|
|
353
354
|
releaseOwnedLock(path, payload);
|
|
354
355
|
};
|
|
356
|
+
// Preserve the exact generation that this compare-and-swap replaced.
|
|
357
|
+
// Windows Client startup uses only this immutable tuple to consume the
|
|
358
|
+
// prior generation's exact PID + CreationDate child manifest.
|
|
359
|
+
const reclaimedOwner = Object.freeze({ ...existing });
|
|
355
360
|
return {
|
|
356
361
|
acquired: true,
|
|
357
362
|
path,
|
|
358
363
|
payload,
|
|
364
|
+
reclaimedOwner,
|
|
359
365
|
release,
|
|
360
366
|
updateRole: nextRole => updateOwnedLockRole(path, payload, nextRole)
|
|
361
367
|
};
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.214",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
"livedesk-client-fast": "bin/livedesk-client-fast.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"bin/",
|
|
14
|
-
"src/",
|
|
15
|
-
"
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"tests/",
|
|
16
|
+
"README.md",
|
|
16
17
|
"THIRD_PARTY_NOTICES.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
@@ -40,10 +41,10 @@
|
|
|
40
41
|
"ws": "^8.18.3"
|
|
41
42
|
},
|
|
42
43
|
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
44
|
+
"@livedesk/fast-linux-x64": "0.1.419",
|
|
45
|
+
"@livedesk/fast-osx-arm64": "0.1.419",
|
|
46
|
+
"@livedesk/fast-osx-x64": "0.1.419",
|
|
47
|
+
"@livedesk/fast-win-x64": "0.1.419"
|
|
47
48
|
},
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
+
linkSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
readFileSync,
|
|
6
7
|
renameSync,
|
|
@@ -85,6 +86,10 @@ function normalizeRecords(values) {
|
|
|
85
86
|
return [...unique.values()];
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
function sourceRevision(sourceText) {
|
|
90
|
+
return createHash('sha256').update(String(sourceText || ''), 'utf8').digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
function writeJsonAtomic(path, value) {
|
|
89
94
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
90
95
|
const temporaryPath = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
@@ -157,19 +162,16 @@ export function writeWindowsOwnedProcessManifest({
|
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
* Reads only a fresh manifest belonging to the exact runtime-lock generation.
|
|
162
|
-
* Owner mismatch, malformed identity, expiry, and PID reuse all fail closed.
|
|
163
|
-
*/
|
|
164
|
-
export function readWindowsOwnedProcessManifest({
|
|
165
|
+
function readExactWindowsOwnedProcessManifest({
|
|
165
166
|
stateDir,
|
|
166
167
|
owner,
|
|
167
|
-
maxAgeMs =
|
|
168
|
+
maxAgeMs = null,
|
|
169
|
+
includeRevision = false,
|
|
168
170
|
now
|
|
169
171
|
} = {}) {
|
|
170
172
|
const exactOwner = normalizeOwner(owner);
|
|
171
173
|
const nowMs = resolveNow(now);
|
|
172
|
-
const maximumAge = Number(maxAgeMs);
|
|
174
|
+
const maximumAge = maxAgeMs === null ? null : Number(maxAgeMs);
|
|
173
175
|
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
174
176
|
if (!exactOwner) {
|
|
175
177
|
return {
|
|
@@ -179,8 +181,8 @@ export function readWindowsOwnedProcessManifest({
|
|
|
179
181
|
};
|
|
180
182
|
}
|
|
181
183
|
if (!Number.isFinite(nowMs)
|
|
182
|
-
||
|
|
183
|
-
|
|
184
|
+
|| (maximumAge !== null
|
|
185
|
+
&& (!Number.isFinite(maximumAge) || maximumAge < 0))) {
|
|
184
186
|
return {
|
|
185
187
|
ok: false,
|
|
186
188
|
records: [],
|
|
@@ -189,10 +191,17 @@ export function readWindowsOwnedProcessManifest({
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
let parsed;
|
|
194
|
+
let sourceText;
|
|
192
195
|
try {
|
|
193
|
-
|
|
196
|
+
sourceText = readFileSync(path, 'utf8');
|
|
197
|
+
parsed = JSON.parse(sourceText);
|
|
194
198
|
} catch (error) {
|
|
195
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
records: [],
|
|
202
|
+
missing: error?.code === 'ENOENT',
|
|
203
|
+
error
|
|
204
|
+
};
|
|
196
205
|
}
|
|
197
206
|
const updatedAt = String(parsed?.updatedAt || '');
|
|
198
207
|
const updatedAtMs = Date.parse(updatedAt);
|
|
@@ -214,7 +223,7 @@ export function readWindowsOwnedProcessManifest({
|
|
|
214
223
|
}
|
|
215
224
|
if (!Number.isFinite(updatedAtMs)
|
|
216
225
|
|| updatedAtMs > nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
217
|
-
|| nowMs - updatedAtMs > maximumAge) {
|
|
226
|
+
|| (maximumAge !== null && nowMs - updatedAtMs > maximumAge)) {
|
|
218
227
|
return {
|
|
219
228
|
ok: false,
|
|
220
229
|
records: [],
|
|
@@ -231,5 +240,163 @@ export function readWindowsOwnedProcessManifest({
|
|
|
231
240
|
error: resultError('Windows LiveDesk process manifest contains an invalid process identity.')
|
|
232
241
|
};
|
|
233
242
|
}
|
|
234
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
records: exactRecords,
|
|
246
|
+
updatedAt,
|
|
247
|
+
...(includeRevision ? { revision: sourceRevision(sourceText) } : {})
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reads only a fresh manifest belonging to the exact runtime-lock generation.
|
|
253
|
+
* Owner mismatch, malformed identity, expiry, and PID reuse all fail closed.
|
|
254
|
+
*/
|
|
255
|
+
export function readWindowsOwnedProcessManifest({
|
|
256
|
+
stateDir,
|
|
257
|
+
owner,
|
|
258
|
+
maxAgeMs = DEFAULT_WINDOWS_OWNED_PROCESS_MANIFEST_MAX_AGE_MS,
|
|
259
|
+
now
|
|
260
|
+
} = {}) {
|
|
261
|
+
return readExactWindowsOwnedProcessManifest({
|
|
262
|
+
stateDir,
|
|
263
|
+
owner,
|
|
264
|
+
maxAgeMs,
|
|
265
|
+
now
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Recovers only the exact manifest generation returned by a successful
|
|
271
|
+
* stale-lock compare-and-swap. Its age is intentionally unbounded because a
|
|
272
|
+
* machine can be restarted long after the launcher and Agent died. Malformed,
|
|
273
|
+
* mismatched, or future-dated manifests still fail closed.
|
|
274
|
+
*/
|
|
275
|
+
export function readDeadWindowsOwnedProcessManifest({
|
|
276
|
+
stateDir,
|
|
277
|
+
owner,
|
|
278
|
+
now
|
|
279
|
+
} = {}) {
|
|
280
|
+
return readExactWindowsOwnedProcessManifest({
|
|
281
|
+
stateDir,
|
|
282
|
+
owner,
|
|
283
|
+
maxAgeMs: null,
|
|
284
|
+
includeRevision: true,
|
|
285
|
+
now
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Atomically quarantines and consumes only the exact dead-owner manifest
|
|
291
|
+
* revision that was drained. If another generation replaces the canonical
|
|
292
|
+
* path between validation and rename, the moved file is restored or preserved
|
|
293
|
+
* instead of ever being deleted.
|
|
294
|
+
*/
|
|
295
|
+
export function consumeDeadWindowsOwnedProcessManifest({
|
|
296
|
+
stateDir,
|
|
297
|
+
owner,
|
|
298
|
+
revision,
|
|
299
|
+
now
|
|
300
|
+
} = {}) {
|
|
301
|
+
const exactOwner = normalizeOwner(owner);
|
|
302
|
+
const expectedRevision = String(revision || '').trim().toLowerCase();
|
|
303
|
+
const nowMs = resolveNow(now);
|
|
304
|
+
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
305
|
+
if (!exactOwner || !/^[a-f0-9]{64}$/.test(expectedRevision)) {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
error: resultError('An exact Windows manifest owner and revision are required for consumption.')
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!Number.isFinite(nowMs)) {
|
|
312
|
+
return { ok: false, error: resultError('A valid manifest consumption time is required.') };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const current = readDeadWindowsOwnedProcessManifest({
|
|
316
|
+
stateDir,
|
|
317
|
+
owner: exactOwner,
|
|
318
|
+
now: nowMs
|
|
319
|
+
});
|
|
320
|
+
if (!current.ok) {
|
|
321
|
+
return current.missing === true
|
|
322
|
+
? { ok: true, path, consumed: false, missing: true }
|
|
323
|
+
: { ...current, path, consumed: false };
|
|
324
|
+
}
|
|
325
|
+
if (current.revision !== expectedRevision) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
path,
|
|
329
|
+
consumed: false,
|
|
330
|
+
error: resultError('Windows LiveDesk process manifest changed after its exact process set was drained.')
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const quarantinePath = `${path}.consume.${process.pid}.${randomBytes(8).toString('hex')}`;
|
|
335
|
+
try {
|
|
336
|
+
renameSync(path, quarantinePath);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return { ok: false, path, quarantinePath, consumed: false, error };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let movedSourceText = '';
|
|
342
|
+
let movedParsed = null;
|
|
343
|
+
try {
|
|
344
|
+
movedSourceText = readFileSync(quarantinePath, 'utf8');
|
|
345
|
+
movedParsed = JSON.parse(movedSourceText);
|
|
346
|
+
} catch {
|
|
347
|
+
// The exact post-rename validation below restores or preserves this
|
|
348
|
+
// file; unreadable content is never evidence that deletion is safe.
|
|
349
|
+
}
|
|
350
|
+
const movedUpdatedAtMs = Date.parse(String(movedParsed?.updatedAt || ''));
|
|
351
|
+
const movedRecords = normalizeRecords(movedParsed?.records);
|
|
352
|
+
const movedIsExact = movedParsed?.protocolVersion === WINDOWS_OWNED_PROCESS_MANIFEST_VERSION
|
|
353
|
+
&& ownersMatch(movedParsed?.owner, exactOwner)
|
|
354
|
+
&& Number.isFinite(movedUpdatedAtMs)
|
|
355
|
+
&& movedUpdatedAtMs <= nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
356
|
+
&& movedRecords !== null
|
|
357
|
+
&& sourceRevision(movedSourceText) === expectedRevision;
|
|
358
|
+
if (!movedIsExact) {
|
|
359
|
+
let restored = false;
|
|
360
|
+
let restoreError = null;
|
|
361
|
+
try {
|
|
362
|
+
// Hard-link publication is create-if-absent. Unlike rename, it
|
|
363
|
+
// can never overwrite a newer canonical generation that appears
|
|
364
|
+
// during restoration.
|
|
365
|
+
linkSync(quarantinePath, path);
|
|
366
|
+
restored = true;
|
|
367
|
+
try { unlinkSync(quarantinePath); } catch { /* both exact links safely preserve the file */ }
|
|
368
|
+
} catch (error) {
|
|
369
|
+
restoreError = error;
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
path,
|
|
374
|
+
quarantinePath: restored ? undefined : quarantinePath,
|
|
375
|
+
consumed: false,
|
|
376
|
+
error: resultError(
|
|
377
|
+
'Windows LiveDesk process manifest changed during consumption; '
|
|
378
|
+
+ `${restored ? 'the moved generation was restored.' : 'the moved generation was preserved.'}`
|
|
379
|
+
+ `${restoreError?.message ? ` ${restoreError.message}` : ''}`
|
|
380
|
+
)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
unlinkSync(quarantinePath);
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
path,
|
|
389
|
+
revision: expectedRevision,
|
|
390
|
+
records: movedRecords,
|
|
391
|
+
consumed: true
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
path,
|
|
397
|
+
quarantinePath,
|
|
398
|
+
consumed: false,
|
|
399
|
+
error
|
|
400
|
+
};
|
|
401
|
+
}
|
|
235
402
|
}
|