@platless/axom-cli 0.6.36 → 0.6.52
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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationPreparation.js +1 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { audioConfiguration, audioMode } from './audio.js';
|
|
3
3
|
import { lobbyRequestSchema, lobbyPlanSchema } from './lobby.js';
|
|
4
|
+
import { deviceAcquisitionSchema, deviceOwnershipCommandSchema, deviceOwnershipEventSchema } from './ownershipProtocol.js';
|
|
4
5
|
export const DESKTOP_MAX_JSON_BYTES = 48 * 1024;
|
|
5
6
|
export class DesktopProtocolError extends Error {
|
|
6
7
|
code = 'invalid_request';
|
|
@@ -39,6 +40,7 @@ const backend = z.strictObject({ id: bounded, kind: z.enum(['software', 'hardwar
|
|
|
39
40
|
formats: z.array(encoderFormat).min(1).max(2).refine(a => distinct(a.map(f => f.format))),
|
|
40
41
|
});
|
|
41
42
|
export const desktopCapabilitiesSchema = z.strictObject({
|
|
43
|
+
deviceOwnershipProtocol: z.literal(1).optional(),
|
|
42
44
|
cursorEmbedded: z.boolean().optional(), cursorVisibility: z.boolean().optional(), lobby: z.boolean().optional(),
|
|
43
45
|
protocols, providerId: bounded, providerVersion: bounded, configured: z.boolean(),
|
|
44
46
|
permission: z.enum(['granted', 'required', 'unknown', 'not_applicable']), health: z.enum(['ready', 'unavailable']), reason: code.nullable(),
|
|
@@ -87,7 +89,7 @@ const feedback = z.strictObject({ epoch: u32, receivedId: int(), decodedId: int(
|
|
|
87
89
|
const resizeResult = z.strictObject({ commandId: id, width: axis, height: axis, displayRevision: u32, outcome: z.enum(['applied', 'rolled_back', 'failed']), dpi: dpi.optional(), captureRegion: captureRegion.optional() });
|
|
88
90
|
export const desktopErrorCodeSchema = z.enum(['unavailable', 'unknown_capability', 'unsupported_protocol', 'unsupported_client',
|
|
89
91
|
'permission_required', 'busy', 'conflict', 'invalid_request', 'rate_limited', 'startup_timeout', 'encoder_failed', 'decoder_failed',
|
|
90
|
-
'resize_failed', 'transport_failed', 'authorization_lost', 'takeover_lost']);
|
|
92
|
+
'resize_failed', 'transport_failed', 'authorization_lost', 'takeover_lost', 'upgrade_required']);
|
|
91
93
|
const state = z.enum(['REQUESTED', 'PREPARING', 'NEGOTIATING', 'STARTING', 'ACTIVE', 'RECONFIGURING', 'SUSPENDED', 'RECONNECTING', 'FAILED', 'CLOSED']);
|
|
92
94
|
const clipboardChunk = { controlGeneration: positive, requestId: id, revision: int().default(0), offset: int(0, 1048576).default(0),
|
|
93
95
|
total: int(0, 1048576).default(0), data: z.string().max(10924).default('') };
|
|
@@ -153,15 +155,21 @@ const sessionRequest = z.strictObject({ requestId: id, displayId: id, viewer,
|
|
|
153
155
|
viewport: z.strictObject({ widthDp: z.number().positive().max(16384), heightDp: z.number().positive().max(16384),
|
|
154
156
|
deviceScale: z.number().min(0.5).max(8), orientation: z.enum(['landscape', 'portrait']) }),
|
|
155
157
|
resolution: z.discriminatedUnion('mode', [z.strictObject({ mode: z.literal('fit') }), z.strictObject({ mode: z.literal('fixed'), width: axis, height: axis })]),
|
|
156
|
-
quality, takeover: z.boolean().default(false), lobbyId: id.optional(), planId: id.optional(), mediaTransport: z.enum(['quic', 'wss']).optional()
|
|
157
|
-
.
|
|
158
|
+
quality, takeover: z.boolean().default(false), lobbyId: id.optional(), planId: id.optional(), mediaTransport: z.enum(['quic', 'wss']).optional(),
|
|
159
|
+
ownershipProtocol: z.literal(1).optional(), expectedOwnershipRevision: int().optional(), expectedDeviceGeneration: bounded.optional(), deviceAcquisition: deviceAcquisitionSchema.optional() })
|
|
160
|
+
.refine(request => (request.lobbyId == null) === (request.planId == null) && (request.lobbyId == null) === (request.mediaTransport == null) &&
|
|
161
|
+
(request.deviceAcquisition?.kind !== 'takeover' || request.takeover && request.ownershipProtocol === 1 && request.expectedOwnershipRevision != null) &&
|
|
162
|
+
(request.expectedOwnershipRevision == null) === (request.expectedDeviceGeneration == null) &&
|
|
163
|
+
(request.expectedOwnershipRevision == null || request.ownershipProtocol === 1 && request.takeover));
|
|
158
164
|
const desktopManagementSchema = z.discriminatedUnion('t', [
|
|
165
|
+
deviceOwnershipCommandSchema,
|
|
166
|
+
deviceOwnershipEventSchema,
|
|
159
167
|
z.strictObject({ t: z.literal('desktop_lobby'), commandId: id, lobbyId: id, deviceGeneration: bounded, request: z.lazy(() => lobbyRequestSchema),
|
|
160
168
|
wssProbeGrant: z.string().regex(/^[A-Za-z0-9_-]{43}$/).nullish(), quicProbeGrant: z.string().regex(/^[A-Za-z0-9_-]{43}$/).nullish(),
|
|
161
169
|
quic: z.strictObject({ port: int(1, 65535), certificate: z.string().min(64).max(2048) }).nullish() }),
|
|
162
170
|
z.strictObject({ t: z.literal('desktop_background'), commandId: id, lobbyId: id, deviceGeneration: bounded,
|
|
163
171
|
planId: id, revision: positive, targetId: id, grant: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
164
|
-
cachedVersion: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).nullish() }),
|
|
172
|
+
cachedVersion: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).nullish(), pixelWidth: int(2, 4096), pixelHeight: int(2, 4096) }),
|
|
165
173
|
z.strictObject({ t: z.literal('desktop_lobby_result'), commandId: id, lobbyId: id, deviceGeneration: bounded,
|
|
166
174
|
plan: z.lazy(() => lobbyPlanSchema).nullable(), code: desktopErrorCodeSchema.nullable() }),
|
|
167
175
|
z.strictObject({ t: z.literal('workstation_capabilities'), ...capabilityReportSchema.shape }),
|
|
@@ -171,12 +179,13 @@ const desktopManagementSchema = z.discriminatedUnion('t', [
|
|
|
171
179
|
candidate: z.boolean().optional(), mediaTransport: z.enum(['quic', 'wss']).optional() }),
|
|
172
180
|
z.strictObject({ t: z.literal('desktop_prepare_result'), commandId: id, sessionId: id,
|
|
173
181
|
state: z.enum(['bound', 'failed']), code: desktopErrorCodeSchema.nullable() }),
|
|
174
|
-
z.strictObject({ t: z.literal('desktop_cancel'), commandId: id, sessionId: id, controlGeneration: positive, code: desktopErrorCodeSchema }),
|
|
182
|
+
z.strictObject({ t: z.literal('desktop_cancel'), commandId: id, sessionId: id, controlGeneration: positive, code: desktopErrorCodeSchema, requireIsolation: z.boolean().optional() }),
|
|
183
|
+
z.strictObject({ t: z.literal('desktop_isolation_result'), commandId: id, sessionId: id, deviceGeneration: bounded, controlGeneration: positive, isolated: z.boolean() }),
|
|
175
184
|
z.strictObject({ t: z.literal('desktop_session_state'), sessionId: id, controlGeneration: positive, state, code: desktopErrorCodeSchema.nullable() }),
|
|
176
185
|
]);
|
|
177
186
|
export function decodeDesktopManagementFrame(source, from) {
|
|
178
187
|
const frame = checked(desktopManagementSchema, source);
|
|
179
|
-
const sender = ['desktop_lobby_result', 'workstation_capabilities', 'desktop_prepare_result', 'desktop_session_state'].includes(frame.t) ? 'device' : 'server';
|
|
188
|
+
const sender = ['desktop_ownership_result', 'desktop_isolation_result', 'desktop_lobby_result', 'workstation_capabilities', 'desktop_prepare_result', 'desktop_session_state'].includes(frame.t) ? 'device' : 'server';
|
|
180
189
|
if (from && from !== sender)
|
|
181
190
|
return invalid();
|
|
182
191
|
return frame;
|
|
@@ -14,6 +14,7 @@ const diagnosticCodes = new Set(['invalid_request', 'unavailable', 'conflict', '
|
|
|
14
14
|
/** One authority owner and bounded management work queue; receive/heartbeat never await native I/O. */
|
|
15
15
|
export class DesktopSessionSupervisor {
|
|
16
16
|
options;
|
|
17
|
+
get activeCount() { return Number(!!this.active && !this.active.retired) + this.candidates.size; }
|
|
17
18
|
generation;
|
|
18
19
|
active;
|
|
19
20
|
candidates = new Map();
|
|
@@ -28,6 +29,7 @@ export class DesktopSessionSupervisor {
|
|
|
28
29
|
backgroundTail = Promise.resolve();
|
|
29
30
|
backgroundJobs = 0;
|
|
30
31
|
cleanupJobs = 0;
|
|
32
|
+
retainedSessions = new Map();
|
|
31
33
|
maintenance;
|
|
32
34
|
constructor(options) {
|
|
33
35
|
this.options = options;
|
|
@@ -90,8 +92,28 @@ export class DesktopSessionSupervisor {
|
|
|
90
92
|
return this.planLobby(frame);
|
|
91
93
|
if (frame.t === 'desktop_prepare')
|
|
92
94
|
return this.prepare(frame);
|
|
95
|
+
if (frame.requireIsolation)
|
|
96
|
+
return this.isolate(frame, generation);
|
|
93
97
|
return this.cancel(frame.sessionId, { commandId: frame.commandId, deviceGeneration: generation, controlGeneration: frame.controlGeneration });
|
|
94
98
|
}
|
|
99
|
+
async isolate(frame, generation) {
|
|
100
|
+
const key = `${generation}:${frame.sessionId}:${frame.controlGeneration}`;
|
|
101
|
+
const retained = this.retainedSessions.get(key);
|
|
102
|
+
let isolated = false;
|
|
103
|
+
if (retained) {
|
|
104
|
+
this.retire(retained);
|
|
105
|
+
try {
|
|
106
|
+
await this.cleanup(retained);
|
|
107
|
+
isolated = true;
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
}
|
|
111
|
+
if (this.generation === generation && !this.closed)
|
|
112
|
+
this.options.report(generation, {
|
|
113
|
+
t: 'desktop_isolation_result', commandId: frame.commandId, sessionId: frame.sessionId,
|
|
114
|
+
deviceGeneration: generation, controlGeneration: frame.controlGeneration, isolated,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
95
117
|
background(frame) {
|
|
96
118
|
if (this.closed || frame.deviceGeneration !== this.generation || this.backgroundJobs >= 16)
|
|
97
119
|
return Promise.resolve();
|
|
@@ -103,7 +125,7 @@ export class DesktopSessionSupervisor {
|
|
|
103
125
|
if (!current() || !this.options.sockets.uploadWallpaper)
|
|
104
126
|
return;
|
|
105
127
|
try {
|
|
106
|
-
const wallpaper = this.options.provider.wallpaper ? await bounded(this.options.provider.wallpaper(frame.targetId), 8000) : null;
|
|
128
|
+
const wallpaper = this.options.provider.wallpaper ? await bounded(this.options.provider.wallpaper(frame.targetId, { width: frame.pixelWidth, height: frame.pixelHeight }), 8000) : null;
|
|
107
129
|
if (current())
|
|
108
130
|
await this.options.sockets.uploadWallpaper(frame.grant, wallpaper, wallpaper?.asset.version === frame.cachedVersion);
|
|
109
131
|
}
|
|
@@ -184,12 +206,30 @@ export class DesktopSessionSupervisor {
|
|
|
184
206
|
this.tail = next.catch(() => { }).finally(() => { this.pending--; });
|
|
185
207
|
return next;
|
|
186
208
|
}
|
|
209
|
+
preparationCurrent(binding) {
|
|
210
|
+
if (this.options.preparationCurrent)
|
|
211
|
+
return this.options.preparationCurrent(binding);
|
|
212
|
+
let begun = false;
|
|
213
|
+
return (beginEffect = false) => {
|
|
214
|
+
if (this.closed || binding.deviceGeneration !== this.generation || this.options.controllerCurrent?.(binding) === false)
|
|
215
|
+
throw new NativeCommandError('control_lost');
|
|
216
|
+
if (!begun && this.options.newWorkAllowed?.() === false)
|
|
217
|
+
return false;
|
|
218
|
+
if (beginEffect)
|
|
219
|
+
begun = true;
|
|
220
|
+
return true;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
187
223
|
prepare(command) {
|
|
188
224
|
if (this.maintenance)
|
|
189
225
|
return this.maintenance.catch(() => { }).then(() => this.prepare(command));
|
|
190
226
|
decodeDesktopManagementFrame(JSON.stringify({ ...command, t: 'desktop_prepare' }), 'server');
|
|
191
227
|
if (this.closed || command.deviceGeneration !== this.generation)
|
|
192
228
|
return Promise.resolve();
|
|
229
|
+
if (this.options.newWorkAllowed?.() === false) {
|
|
230
|
+
this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'failed', code: 'busy' });
|
|
231
|
+
return Promise.resolve();
|
|
232
|
+
}
|
|
193
233
|
mediaRequire(this.pending < 16);
|
|
194
234
|
const key = `${command.deviceGeneration}:${command.commandId}`, serialized = canonicalDesktopJson(command);
|
|
195
235
|
const previousCommand = this.commands.get(key);
|
|
@@ -201,8 +241,12 @@ export class DesktopSessionSupervisor {
|
|
|
201
241
|
this.commands.set(key, serialized);
|
|
202
242
|
mediaRequire(!command.candidate || this.candidates.size < 8);
|
|
203
243
|
const previous = command.candidate ? undefined : this.active;
|
|
204
|
-
|
|
244
|
+
const openingCurrent = previous ? this.preparationCurrent(command) : () => this.options.newWorkAllowed?.() !== false;
|
|
245
|
+
if (previous) {
|
|
246
|
+
if (!openingCurrent(true))
|
|
247
|
+
return Promise.resolve();
|
|
205
248
|
this.retire(previous);
|
|
249
|
+
}
|
|
206
250
|
const s = { diagnosticId: ++this.diagnosticId, diagnosticCount: 0, rateDiagnosticCount: 0, lastRateDiagnosticAt: -Infinity,
|
|
207
251
|
performanceDiagnosticCount: 0, lastPerformanceDiagnosticAt: -Infinity, diagnosticKey: randomBytes(16).toString('hex'), motionEncodeMs: 0,
|
|
208
252
|
binding: { ...command }, abort: new AbortController(), retired: false, viewerReady: false, starting: false,
|
|
@@ -210,6 +254,8 @@ export class DesktopSessionSupervisor {
|
|
|
210
254
|
controller: new DesktopRateController({ fpsCeiling: 30, maxFps: 30 }), rateBusy: false, capturePaused: false, lastFeedbackBytes: 0,
|
|
211
255
|
activeBytes: 0, queuedAt: 0, lastPacketAt: performance.now(), receivedFrame: 0, firstCompleteFrame: 0, flow: new DesktopMediaFlowWindow(), delivery: new DesktopDeliveryDelay(), packetLoop: false, mediaWake: new Set(), nativeCreditDirty: false, nativeDisposed: { motion: 0, cursor: 0, exact: 0 }, configAdmittedEpoch: 0, recovery: [],
|
|
212
256
|
overviewBudget: new DesktopOverviewBudget(), overviewSequence: 0, overviewInitialPolls: 0, challenges: new Map(), rtts: [], rtt: 0, nextPing: performance.now() + 10000 };
|
|
257
|
+
mediaRequire(this.retainedSessions.size < 1024);
|
|
258
|
+
this.retainedSessions.set(`${command.deviceGeneration}:${command.sessionId}:${command.controlGeneration}`, s);
|
|
213
259
|
if (command.candidate)
|
|
214
260
|
this.candidates.set(s.binding.sessionId, s);
|
|
215
261
|
else
|
|
@@ -219,6 +265,12 @@ export class DesktopSessionSupervisor {
|
|
|
219
265
|
await this.cleanup(previous);
|
|
220
266
|
if (!this.current(s))
|
|
221
267
|
return;
|
|
268
|
+
if (!openingCurrent()) {
|
|
269
|
+
this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'failed', code: 'busy' });
|
|
270
|
+
this.retire(s);
|
|
271
|
+
await this.cleanup(s);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
222
274
|
mediaRequire(!this.failedCleanup);
|
|
223
275
|
try {
|
|
224
276
|
// Discovery is side-effect free and independent of the authenticated socket attach.
|
|
@@ -280,7 +332,8 @@ export class DesktopSessionSupervisor {
|
|
|
280
332
|
}
|
|
281
333
|
current(s) {
|
|
282
334
|
return !this.closed && !s.retired &&
|
|
283
|
-
(this.active === s || this.candidates.get(s.binding.sessionId) === s) && this.generation === s.binding.deviceGeneration
|
|
335
|
+
(this.active === s || this.candidates.get(s.binding.sessionId) === s) && this.generation === s.binding.deviceGeneration &&
|
|
336
|
+
(s.binding.candidate || this.options.controllerCurrent?.(s.binding) !== false);
|
|
284
337
|
}
|
|
285
338
|
diagnostic(s, origin, error, packet, command) {
|
|
286
339
|
// At most 16 fixed-shape records per session, before recovery resets its state.
|
|
@@ -527,6 +580,10 @@ export class DesktopSessionSupervisor {
|
|
|
527
580
|
}
|
|
528
581
|
return;
|
|
529
582
|
}
|
|
583
|
+
if (message.type === 'select' && this.options.newWorkAllowed?.() === false) {
|
|
584
|
+
await this.send(s, { v: 1, type: 'error', sessionId: s.binding.sessionId, payload: { code: 'busy', retryable: true } });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
530
587
|
if ('commandId' in message.payload && message.type !== 'audio_ready' && !message.type.startsWith('motion_cut_') && !(resumeAudio && message.type === 'audio_select')) {
|
|
531
588
|
const id = message.payload.commandId, body = canonicalDesktopJson(message);
|
|
532
589
|
const previous = s.commands.get(id);
|
|
@@ -573,24 +630,60 @@ export class DesktopSessionSupervisor {
|
|
|
573
630
|
return;
|
|
574
631
|
}
|
|
575
632
|
case 'select': {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
633
|
+
const operationCurrent = this.preparationCurrent(s.binding);
|
|
634
|
+
const busy = async () => {
|
|
635
|
+
if (s.commands.get(message.payload.commandId) === canonicalDesktopJson(message))
|
|
636
|
+
s.commands.delete(message.payload.commandId);
|
|
637
|
+
if (this.current(s))
|
|
638
|
+
await this.send(s, { v: 1, type: 'error', sessionId: s.binding.sessionId, payload: { code: 'busy', retryable: true } });
|
|
639
|
+
};
|
|
640
|
+
if (this.candidates.get(s.binding.sessionId) === s && !await this.enqueue(async () => {
|
|
641
|
+
mediaRequire(this.current(s));
|
|
642
|
+
if (!operationCurrent())
|
|
643
|
+
return false;
|
|
644
|
+
const previous = this.active;
|
|
645
|
+
if (previous) {
|
|
646
|
+
if (!operationCurrent(true))
|
|
647
|
+
return false;
|
|
648
|
+
this.retire(previous);
|
|
649
|
+
await this.cleanup(previous);
|
|
650
|
+
}
|
|
651
|
+
mediaRequire(this.current(s) && !this.failedCleanup);
|
|
652
|
+
if (!operationCurrent())
|
|
653
|
+
return false;
|
|
654
|
+
this.candidates.delete(s.binding.sessionId);
|
|
655
|
+
this.active = s;
|
|
656
|
+
s.binding.candidate = false;
|
|
657
|
+
return true;
|
|
658
|
+
})) {
|
|
659
|
+
await busy();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!operationCurrent()) {
|
|
663
|
+
await busy();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
588
666
|
mediaRequire(!s.backend && !s.selection);
|
|
589
667
|
s.selection = message.payload;
|
|
590
668
|
s.configuredCommand = message.payload.commandId;
|
|
591
669
|
s.controller = new DesktopRateController({ fpsCeiling: message.payload.fpsCeiling, maxFps: message.payload.fpsCeiling });
|
|
592
|
-
s.preparation = this.options.provider.prepare(message.payload, s.binding);
|
|
593
|
-
|
|
670
|
+
s.preparation = this.options.provider.prepare(message.payload, s.binding, operationCurrent);
|
|
671
|
+
let backend;
|
|
672
|
+
try {
|
|
673
|
+
backend = await s.preparation;
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (error instanceof NativeCommandError && error.code === 'busy') {
|
|
677
|
+
if (s.selection === message.payload) {
|
|
678
|
+
s.selection = undefined;
|
|
679
|
+
s.configuredCommand = undefined;
|
|
680
|
+
s.preparation = undefined;
|
|
681
|
+
}
|
|
682
|
+
await busy();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
594
687
|
if (!this.current(s)) {
|
|
595
688
|
await backend.stop();
|
|
596
689
|
return;
|
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { wallpaperAssetSchema } from './lobby.js';
|
|
3
|
+
export const WALLPAPER_MAX_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
export const WALLPAPER_MAX_JSON = 3 * 1024 * 1024;
|
|
3
5
|
export function decodeWallpaper(text) {
|
|
4
|
-
if (Buffer.byteLength(text) >
|
|
6
|
+
if (Buffer.byteLength(text) > WALLPAPER_MAX_JSON)
|
|
5
7
|
throw new Error('wallpaper_unavailable');
|
|
6
8
|
const result = JSON.parse(text);
|
|
7
9
|
const asset = wallpaperAssetSchema.parse(result.asset);
|
|
8
|
-
if (typeof result.
|
|
10
|
+
if (typeof result.image !== 'string' || result.image.length > Math.ceil(WALLPAPER_MAX_BYTES / 3) * 4)
|
|
9
11
|
throw new Error('wallpaper_unavailable');
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
+
const bytes = Buffer.from(result.image, 'base64');
|
|
13
|
+
if (bytes.length < 20 || bytes.length > WALLPAPER_MAX_BYTES || bytes.toString('base64') !== result.image ||
|
|
14
|
+
bytes.toString('ascii', 0, 4) !== 'RIFF' || bytes.toString('ascii', 8, 12) !== 'WEBP' || bytes.readUInt32LE(4) + 8 !== bytes.length || asset.mimeType !== 'image/webp')
|
|
12
15
|
throw new Error('wallpaper_unavailable');
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
let dimensions;
|
|
17
|
+
for (let offset = 12; offset + 8 <= bytes.length;) {
|
|
18
|
+
const kind = bytes.toString('ascii', offset, offset + 4), size = bytes.readUInt32LE(offset + 4), data = offset + 8;
|
|
19
|
+
if (size > bytes.length - data || kind === 'ANIM' || kind === 'ANMF')
|
|
20
|
+
throw new Error('wallpaper_unavailable');
|
|
21
|
+
if (kind === 'VP8X' && (size !== 10 || (bytes[data] & 2)))
|
|
22
|
+
throw new Error('wallpaper_unavailable');
|
|
23
|
+
if (kind === 'VP8 ' && size >= 10 && !(bytes[data] & 1) && bytes[data + 3] === 0x9d && bytes[data + 4] === 1 && bytes[data + 5] === 0x2a)
|
|
24
|
+
dimensions = [bytes.readUInt16LE(data + 6) & 0x3fff, bytes.readUInt16LE(data + 8) & 0x3fff];
|
|
25
|
+
if (kind === 'VP8L' && size >= 5 && bytes[data] === 0x2f) {
|
|
26
|
+
const bits = bytes.readUInt32LE(data + 1);
|
|
27
|
+
dimensions = [(bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1];
|
|
28
|
+
}
|
|
29
|
+
offset = data + size + (size & 1);
|
|
30
|
+
}
|
|
31
|
+
if (!dimensions || dimensions[0] !== asset.width || dimensions[1] !== asset.height)
|
|
32
|
+
throw new Error('wallpaper_unavailable');
|
|
33
|
+
const version = createHash('sha256').update(JSON.stringify(asset)).update(bytes).digest('hex');
|
|
34
|
+
return { asset: { ...asset, version }, bytes };
|
|
15
35
|
}
|
|
16
36
|
export class XfceWallpaperReader {
|
|
17
37
|
execute;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { createHash, randomUUID } from 'node:crypto';
|
|
13
13
|
import { KeeperFrameTooLargeError, KeeperNotConnectedError, KeeperReplacementRequiredError, KeeperRequestError } from '../keepers/kit/client.js';
|
|
14
14
|
import { KeeperLaunchError } from '../keepers/terminal/client.js';
|
|
15
|
+
import { RegistrationOwnershipError } from '../runtime/ownership.js';
|
|
15
16
|
import { EXEC_CAPABILITY, EXEC_SIGNALS, EXEC_TERMINAL_TTL_MS, ExecErrorCode } from '../keepers/terminal/protocol.js';
|
|
16
17
|
import { execArgv, execCwd, execDeadline, execEnvironment } from './command.js';
|
|
17
18
|
import { DurableExec, EXEC_KILL_GRACE_MS } from './durable.js';
|
|
@@ -289,6 +290,8 @@ export class ExecPolicy {
|
|
|
289
290
|
hello = (await keeper.ensure()).hello;
|
|
290
291
|
}
|
|
291
292
|
catch (error) {
|
|
293
|
+
if (error instanceof RegistrationOwnershipError)
|
|
294
|
+
throw error;
|
|
292
295
|
// A repeat may be running in a keeper-terminal this daemon cannot reach right now: running it here too could run it twice.
|
|
293
296
|
if (how.repeat) {
|
|
294
297
|
this.log(`exec ${req.id}: repeated request while keeper-terminal is unreachable: ${messageOf(error)}`);
|
|
@@ -323,6 +326,8 @@ export class ExecPolicy {
|
|
|
323
326
|
return { kind: 'durable', exec: DurableExec.follow(keeper, descriptor, { outlivesChannel, ...(wire ? { wire } : {}) }, this.durableOptions()) };
|
|
324
327
|
}
|
|
325
328
|
catch (error) {
|
|
329
|
+
if (error instanceof RegistrationOwnershipError)
|
|
330
|
+
throw error;
|
|
326
331
|
// Another keeper-terminal generation owns an exec of this id (spec §3.1: two generations may coexist): it exists, so it is
|
|
327
332
|
// never started again here and never run in this process.
|
|
328
333
|
if (errorCode(error) === ExecErrorCode.foreign) {
|
|
@@ -78,6 +78,14 @@ export class FilesHostClient {
|
|
|
78
78
|
return 0;
|
|
79
79
|
return this.lastStatus.uploads + this.lastStatus.encodes;
|
|
80
80
|
}
|
|
81
|
+
/** Removal must distinguish an idle host from one that has not supplied a current activity sample. */
|
|
82
|
+
activitySnapshot() {
|
|
83
|
+
return {
|
|
84
|
+
known: this.stopped ? !this.child && !this.spawning : !!this.child && this.hostUp && !this.retiring
|
|
85
|
+
&& !!this.lastStatus && Date.now() - this.lastStatusAt <= this.timings.hungPinMs,
|
|
86
|
+
transfers: this.activity() + this.sent.size + this.waiting,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
81
89
|
/** Process id of the current host (diagnostics and tests). */
|
|
82
90
|
get pid() { return this.child?.pid; }
|
|
83
91
|
get isReady() { return this.ready; }
|
|
@@ -392,7 +400,8 @@ export class FilesHostClient {
|
|
|
392
400
|
// Past the grace, stop feeding it: new requests wait for the restarted host while the ones in flight finish.
|
|
393
401
|
if (overdue)
|
|
394
402
|
this.ready = false;
|
|
395
|
-
if (this.sent.size === 0 && (overdue ||
|
|
403
|
+
if (this.sent.size === 0 && (overdue || !!this.lastStatus
|
|
404
|
+
&& this.lastStatus.uploads === 0 && this.lastStatus.encodes === 0 && this.lastStatus.inFlight === 0)) {
|
|
396
405
|
this.restartRequestedAt = null;
|
|
397
406
|
this.log('[files-host] workspaces changed; restarting');
|
|
398
407
|
void this.shutdownChild(true);
|
|
@@ -19,6 +19,7 @@ let tools;
|
|
|
19
19
|
let initializing = false;
|
|
20
20
|
let closing;
|
|
21
21
|
const inFlight = new Map();
|
|
22
|
+
const operations = new Set();
|
|
22
23
|
function send(message) {
|
|
23
24
|
if (!process.connected)
|
|
24
25
|
return;
|
|
@@ -64,7 +65,7 @@ function run(request) {
|
|
|
64
65
|
inFlight.set(request.id, controller);
|
|
65
66
|
const deadline = request.deadlineAt === null ? undefined : setTimeout(() => controller.abort(), Math.max(0, request.deadlineAt - Date.now()));
|
|
66
67
|
const current = tools;
|
|
67
|
-
|
|
68
|
+
const operation = (async () => {
|
|
68
69
|
let reply;
|
|
69
70
|
try {
|
|
70
71
|
// runFileFrame never throws: errors (and cancellation) come back as the result frame's `error`.
|
|
@@ -89,6 +90,8 @@ function run(request) {
|
|
|
89
90
|
status();
|
|
90
91
|
send(reply);
|
|
91
92
|
})();
|
|
93
|
+
operations.add(operation);
|
|
94
|
+
void operation.finally(() => operations.delete(operation));
|
|
92
95
|
}
|
|
93
96
|
/** Refuses new work, lets running calls finish within a budget (a hung IO cannot hold the exit forever), then exits. */
|
|
94
97
|
function shutdown(code) {
|
|
@@ -96,7 +99,11 @@ function shutdown(code) {
|
|
|
96
99
|
for (const controller of inFlight.values())
|
|
97
100
|
controller.abort();
|
|
98
101
|
const budget = new Promise(resolve => { setTimeout(resolve, DISPOSE_BUDGET_MS).unref(); });
|
|
99
|
-
|
|
102
|
+
const dispose = async () => {
|
|
103
|
+
await Promise.allSettled([...operations]);
|
|
104
|
+
await tools?.dispose();
|
|
105
|
+
};
|
|
106
|
+
await Promise.race([dispose().catch(() => { }), budget]);
|
|
100
107
|
process.exit(code);
|
|
101
108
|
})();
|
|
102
109
|
return closing;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs/promises';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { assertRegistrationOwner } from './runtime/ownership.js';
|
|
18
19
|
const SUFFIX = '.jsonl';
|
|
19
20
|
function parseEntry(line) {
|
|
20
21
|
try {
|
|
@@ -58,7 +59,7 @@ export async function listLegacyJournalRefs(dir) {
|
|
|
58
59
|
return refs.sort();
|
|
59
60
|
}
|
|
60
61
|
/** Every readable entry of `sessionRef`'s journal, in file order (unreadable lines skipped). A missing file is empty. */
|
|
61
|
-
export async function readLegacyJournal(dir, sessionRef) {
|
|
62
|
+
export async function readLegacyJournal(dir, sessionRef, ownership) {
|
|
62
63
|
let text;
|
|
63
64
|
try {
|
|
64
65
|
text = await fs.readFile(fileOf(dir, sessionRef), 'utf8');
|
|
@@ -73,8 +74,10 @@ export async function readLegacyJournal(dir, sessionRef) {
|
|
|
73
74
|
if (line.length === 0)
|
|
74
75
|
continue;
|
|
75
76
|
const entry = parseEntry(line);
|
|
76
|
-
if (entry)
|
|
77
|
+
if (entry) {
|
|
78
|
+
assertRegistrationOwner(JSON.parse(line), ownership, { directory: dir, kind: 'journal' });
|
|
77
79
|
entries.push(entry);
|
|
80
|
+
}
|
|
78
81
|
}
|
|
79
82
|
return entries;
|
|
80
83
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Daemon-side: a typed `acp.*` client for keeper-acp (launch through KeeperLauncher, one long-lived KeeperClient per generation).
|
|
2
2
|
// Exempt from the keeper-side import guard (it runs in the daemon), though it imports nothing but the kit and this keeper's protocol.
|
|
3
3
|
import { KeeperClient } from '../kit/client.js';
|
|
4
|
+
import { assertRegistrationOwner, RegistrationOwnershipError } from '../../runtime/ownership.js';
|
|
4
5
|
import { protocolWindow } from '../kit/protocol.js';
|
|
5
6
|
import { ACP_PROTOCOL_VERSION, acpStream, } from './protocol.js';
|
|
6
7
|
/** keeper-acp could not be reached (launch, connect or hello failed): the request was never sent. */
|
|
@@ -23,7 +24,7 @@ export class AcpKeeperClient {
|
|
|
23
24
|
}
|
|
24
25
|
get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(ACP_PROTOCOL_VERSION); }
|
|
25
26
|
clientFor(paths) {
|
|
26
|
-
if (this.client && this.clientGen === paths.gen)
|
|
27
|
+
if (this.client && this.client.state !== 'closed' && this.clientGen === paths.gen)
|
|
27
28
|
return this.client;
|
|
28
29
|
this.client?.close();
|
|
29
30
|
this.client = new KeeperClient({
|
|
@@ -48,6 +49,7 @@ export class AcpKeeperClient {
|
|
|
48
49
|
get gen() { return this.options.gen; }
|
|
49
50
|
/** The connected client of the current build, launching keeper-acp only when needed (a `gen`-bound client only connects). */
|
|
50
51
|
async ensure() {
|
|
52
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
51
53
|
const current = this.client;
|
|
52
54
|
if (current && current.state === 'ready')
|
|
53
55
|
return current;
|
|
@@ -64,14 +66,43 @@ export class AcpKeeperClient {
|
|
|
64
66
|
return client;
|
|
65
67
|
}
|
|
66
68
|
async call(method, params, options = {}) {
|
|
69
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
67
70
|
let client;
|
|
68
71
|
try {
|
|
69
72
|
client = await this.ensure();
|
|
70
73
|
}
|
|
71
74
|
catch (error) {
|
|
75
|
+
if (error instanceof RegistrationOwnershipError)
|
|
76
|
+
throw error;
|
|
72
77
|
throw new AcpKeeperUnavailableError(error);
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
const ownership = this.options.launcher.ownership;
|
|
80
|
+
const input = params;
|
|
81
|
+
if (ownership && typeof input.sessionKey === 'string') {
|
|
82
|
+
const listed = await client.request('acp.list', {});
|
|
83
|
+
const existing = listed.sessions.find(session => session.sessionKey === input.sessionKey);
|
|
84
|
+
if (existing) {
|
|
85
|
+
if (this.options.launcher.assertSessionOwnership)
|
|
86
|
+
this.options.launcher.assertSessionOwnership(existing.label, this.clientGen);
|
|
87
|
+
else
|
|
88
|
+
assertRegistrationOwner(existing.label, ownership);
|
|
89
|
+
}
|
|
90
|
+
else if (method !== 'acp.spawn')
|
|
91
|
+
throw new RegistrationOwnershipError('Registration session owner is unknown');
|
|
92
|
+
}
|
|
93
|
+
if (ownership && method === 'acp.spawn')
|
|
94
|
+
params = { ...input, label: { ...input.label, registrationId: ownership.registrationId } };
|
|
95
|
+
const result = await client.request(method, params, options);
|
|
96
|
+
if (ownership && method === 'acp.list') {
|
|
97
|
+
const listed = result;
|
|
98
|
+
for (const session of listed.sessions) {
|
|
99
|
+
if (this.options.launcher.assertSessionOwnership)
|
|
100
|
+
this.options.launcher.assertSessionOwnership(session.label, this.clientGen);
|
|
101
|
+
else
|
|
102
|
+
assertRegistrationOwner(session.label, ownership);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
75
106
|
}
|
|
76
107
|
spawn(params, options = {}) { return this.call('acp.spawn', params, options); }
|
|
77
108
|
list() { return this.call('acp.list', {}); }
|
|
@@ -87,13 +118,18 @@ export class AcpKeeperClient {
|
|
|
87
118
|
release(sessionKey) { return this.call('acp.release', { sessionKey }); }
|
|
88
119
|
activity() { return this.call('keeper.activity', {}); }
|
|
89
120
|
async subscribe(sessionKey, stream, handler, options) {
|
|
121
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
90
122
|
let client;
|
|
91
123
|
try {
|
|
92
124
|
client = await this.ensure();
|
|
93
125
|
}
|
|
94
126
|
catch (error) {
|
|
127
|
+
if (error instanceof RegistrationOwnershipError)
|
|
128
|
+
throw error;
|
|
95
129
|
throw new AcpKeeperUnavailableError(error);
|
|
96
130
|
}
|
|
131
|
+
if (this.options.launcher.ownership)
|
|
132
|
+
await this.attach(sessionKey);
|
|
97
133
|
return client.subscribe(acpStream(stream, sessionKey), handler, options);
|
|
98
134
|
}
|
|
99
135
|
/** Drops the connection (keeper-acp and its adapters keep running). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { opendirSync, readFileSync, lstatSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, relative, sep } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* The keeper buildVersion: sha256 over the bundle bytes plus the sorted list of
|
|
@@ -14,22 +14,46 @@ import { join, relative, sep } from 'node:path';
|
|
|
14
14
|
* unchanged keeper code does not change it.
|
|
15
15
|
*/
|
|
16
16
|
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
17
|
-
function walkFiles(dir) {
|
|
17
|
+
function walkFiles(dir, limits) {
|
|
18
18
|
const out = [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
let entries = 0;
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
const walk = (directory, depth) => {
|
|
22
|
+
if (limits && depth > limits.maxDepth)
|
|
23
|
+
throw Error('Native bundle depth limit');
|
|
24
|
+
const handle = opendirSync(directory, { bufferSize: 32 });
|
|
25
|
+
try {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const entry = handle.readSync();
|
|
28
|
+
if (!entry)
|
|
29
|
+
break;
|
|
30
|
+
if (limits && ++entries > limits.maxEntries)
|
|
31
|
+
throw Error('Native bundle entry limit');
|
|
32
|
+
const full = join(directory, entry.name);
|
|
33
|
+
if (entry.isDirectory())
|
|
34
|
+
walk(full, depth + 1);
|
|
35
|
+
else {
|
|
36
|
+
if (limits?.maxBytes !== undefined) {
|
|
37
|
+
bytes += lstatSync(full).size;
|
|
38
|
+
if (bytes > limits.maxBytes)
|
|
39
|
+
throw Error('Native bundle byte limit');
|
|
40
|
+
}
|
|
41
|
+
out.push(full);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
handle.closeSync();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
walk(dir, 0);
|
|
26
50
|
return out;
|
|
27
51
|
}
|
|
28
52
|
/** Relative (posix) paths of the native files carried next to a bundle, sorted. */
|
|
29
|
-
export function nativeFilesOf(kindDir) {
|
|
53
|
+
export function nativeFilesOf(kindDir, limits) {
|
|
30
54
|
let files;
|
|
31
55
|
try {
|
|
32
|
-
files = walkFiles(join(kindDir, 'node_modules'));
|
|
56
|
+
files = walkFiles(join(kindDir, 'node_modules'), limits);
|
|
33
57
|
}
|
|
34
58
|
catch (error) {
|
|
35
59
|
if (error.code === 'ENOENT')
|
|
@@ -24,7 +24,7 @@ export class KeeperRequestTimeoutError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
export class KeeperNotConnectedError extends Error {
|
|
26
26
|
code = 'not_connected';
|
|
27
|
-
constructor(message = 'Keeper is not connected') { super(message); this.name = 'KeeperNotConnectedError'; }
|
|
27
|
+
constructor(message = 'Keeper is not connected', options) { super(message, options); this.name = 'KeeperNotConnectedError'; }
|
|
28
28
|
}
|
|
29
29
|
export class KeeperClosedError extends Error {
|
|
30
30
|
code = 'closed';
|
|
@@ -154,7 +154,7 @@ function connectLink(socketPath, limits, timeoutMs, onEvt, onClose) {
|
|
|
154
154
|
const timer = setTimeout(() => fail(new KeeperNotConnectedError('Timed out connecting to the keeper')), timeoutMs);
|
|
155
155
|
const fail = (error) => { if (settled)
|
|
156
156
|
return; settled = true; clearTimeout(timer); socket.destroy(); reject(error); };
|
|
157
|
-
socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message)));
|
|
157
|
+
socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message, { cause: error })));
|
|
158
158
|
socket.once('connect', () => {
|
|
159
159
|
if (settled)
|
|
160
160
|
return;
|