@platless/axom-cli 0.5.9 → 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 +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- 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 +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +19 -19
- 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 +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- 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 +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- 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/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 +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- 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/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- 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/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- 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 +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- 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/artifact.js +2 -1
- 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 +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
5
|
+
import { execFile, spawn } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { readRdState } from './state.js';
|
|
11
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
12
|
+
import { withPrivateLock } from '../control/locks.js';
|
|
13
|
+
const execute = promisify(execFile);
|
|
14
|
+
const allowedEnvironment = ['DISPLAY', 'XAUTHORITY', 'XDG_RUNTIME_DIR', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_SESSION_ID', 'XDG_SESSION_TYPE', 'XDG_CURRENT_DESKTOP',
|
|
15
|
+
'AXOM_DESKTOP_RUNTIME_DIR', 'AXOM_DESKTOP_OWNED_DUMMY', 'AXOM_DESKTOP_AUTO_DPI',
|
|
16
|
+
'PULSE_SERVER', 'PULSE_SINK', 'AXOM_DESKTOP_AUDIO_SOURCE', 'ALSA_CONFIG_PATH'];
|
|
17
|
+
const displayPattern = /^:\d+(?:\.\d+)?$/;
|
|
18
|
+
export function chooseSession(candidates, selection) {
|
|
19
|
+
if (selection.session === 'virtual')
|
|
20
|
+
return;
|
|
21
|
+
const selected = candidates.filter(candidate => !selection.display || candidate.display.split('.')[0] === selection.display.split('.')[0]);
|
|
22
|
+
if (selected.some(candidate => candidate.type === 'wayland'))
|
|
23
|
+
throw new Error('The existing desktop uses Wayland; use --rd-session virtual for an independent X11 desktop');
|
|
24
|
+
if (selected.some(candidate => candidate.locked))
|
|
25
|
+
throw new Error('The selected desktop is locked');
|
|
26
|
+
if (selected.some(candidate => candidate.unavailable))
|
|
27
|
+
throw new Error('An existing graphical desktop is unavailable; select an accessible X11 display or use --rd-session virtual');
|
|
28
|
+
const unique = [...new Map(selected.filter(candidate => candidate.type === 'x11').map(candidate => [candidate.display.split('.')[0], candidate])).values()];
|
|
29
|
+
if (unique.length > 1)
|
|
30
|
+
throw new Error('Several X11 desktops are available; select one with --rd-display :N');
|
|
31
|
+
if (!unique.length && (selection.session === 'existing' || selection.display))
|
|
32
|
+
throw new Error('No matching X11 desktop is available');
|
|
33
|
+
return unique[0];
|
|
34
|
+
}
|
|
35
|
+
export async function processStart(pid) {
|
|
36
|
+
const text = await fs.readFile(`/proc/${pid}/stat`, 'utf8');
|
|
37
|
+
return text.slice(text.lastIndexOf(')') + 2).split(' ')[19];
|
|
38
|
+
}
|
|
39
|
+
export async function sessionAlive(session) {
|
|
40
|
+
try {
|
|
41
|
+
if (await processStart(session.processId) !== session.processStart)
|
|
42
|
+
return false;
|
|
43
|
+
if (session.sessionId) {
|
|
44
|
+
const { stdout } = await execute('loginctl', ['show-session', session.sessionId, '-p', 'LockedHint', '-p', 'State'], { timeout: 1500, maxBuffer: 4096 });
|
|
45
|
+
if (/^LockedHint=yes$/m.test(stdout) || /^State=closing$/m.test(stdout))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function sessionEnvironment(pid) {
|
|
55
|
+
const environment = {};
|
|
56
|
+
for (const item of (await fs.readFile(`/proc/${pid}/environ`, 'utf8')).split('\0')) {
|
|
57
|
+
const equals = item.indexOf('=');
|
|
58
|
+
const key = item.slice(0, equals);
|
|
59
|
+
if (equals >= 0 && allowedEnvironment.includes(key))
|
|
60
|
+
environment[key] = item.slice(equals + 1);
|
|
61
|
+
}
|
|
62
|
+
return environment;
|
|
63
|
+
}
|
|
64
|
+
export async function discoverSessions() {
|
|
65
|
+
const sessions = new Map();
|
|
66
|
+
let hasLogind = false;
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execute('loginctl', ['list-sessions', '--no-legend', '--no-pager'], { timeout: 2000, maxBuffer: 32768 });
|
|
69
|
+
hasLogind = true;
|
|
70
|
+
for (const row of stdout.trim().split('\n')) {
|
|
71
|
+
const [id, uid] = row.trim().split(/\s+/);
|
|
72
|
+
if (Number(uid) !== process.getuid?.() || !/^[a-zA-Z0-9_-]+$/.test(id))
|
|
73
|
+
continue;
|
|
74
|
+
const { stdout: details } = await execute('loginctl', ['show-session', id, '-p', 'Type', '-p', 'Display', '-p', 'LockedHint', '-p', 'State', '-p', 'Class', '-p', 'Leader'], { timeout: 1500, maxBuffer: 4096 });
|
|
75
|
+
sessions.set(id, Object.fromEntries(details.trim().split('\n').map(line => line.split('=', 2))));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
hasLogind = false;
|
|
80
|
+
}
|
|
81
|
+
const candidates = [];
|
|
82
|
+
for (const entry of await fs.readdir('/proc').catch(() => [])) {
|
|
83
|
+
if (!/^\d+$/.test(entry))
|
|
84
|
+
continue;
|
|
85
|
+
try {
|
|
86
|
+
const root = `/proc/${entry}`;
|
|
87
|
+
if ((await fs.stat(root)).uid !== process.getuid?.())
|
|
88
|
+
continue;
|
|
89
|
+
const name = (await fs.readFile(`${root}/comm`, 'utf8')).trim();
|
|
90
|
+
if (!/^(xfce4-session|gnome-session.*|gnome-shell|ksmserver|plasmashell|mate-session|lxsession|i3|openbox|sway)$/.test(name))
|
|
91
|
+
continue;
|
|
92
|
+
const environment = await sessionEnvironment(Number(entry));
|
|
93
|
+
const display = environment.DISPLAY;
|
|
94
|
+
if (!display || !displayPattern.test(display))
|
|
95
|
+
continue;
|
|
96
|
+
const sessionEntry = environment.XDG_SESSION_ID ? [environment.XDG_SESSION_ID, sessions.get(environment.XDG_SESSION_ID)] :
|
|
97
|
+
[...sessions].find(([, details]) => details.Display === display.split('.')[0]);
|
|
98
|
+
const [sessionId, details] = sessionEntry ?? [];
|
|
99
|
+
if (hasLogind && (!details || details.Class !== 'user' || details.State === 'closing'))
|
|
100
|
+
continue;
|
|
101
|
+
const type = details?.Type === 'wayland' || environment.XDG_SESSION_TYPE === 'wayland' ? 'wayland' : 'x11';
|
|
102
|
+
environment.XAUTHORITY ??= path.join(os.homedir(), '.Xauthority');
|
|
103
|
+
environment.XDG_RUNTIME_DIR ??= `/run/user/${process.getuid?.()}`;
|
|
104
|
+
const start = await processStart(Number(entry));
|
|
105
|
+
const identity = createHash('sha256').update(`${sessionId ?? ''}:${display}:${entry}:${start}`).digest('hex').slice(0, 32);
|
|
106
|
+
const managed = !hasLogind && environment.AXOM_DESKTOP_OWNED_DUMMY === '1' &&
|
|
107
|
+
environment.AXOM_DESKTOP_RUNTIME_DIR !== undefined && path.isAbsolute(environment.AXOM_DESKTOP_RUNTIME_DIR);
|
|
108
|
+
candidates.push({ identity, display, environment, managed, sessionId, processId: Number(entry), processStart: start, type, locked: details?.LockedHint === 'yes' });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const [sessionId, details] of sessions) {
|
|
115
|
+
if (details.Class !== 'user' || details.State === 'closing' || !['wayland', 'x11'].includes(details.Type) || candidates.some(candidate => candidate.sessionId === sessionId))
|
|
116
|
+
continue;
|
|
117
|
+
const display = displayPattern.test(details.Display ?? '') ? details.Display : '';
|
|
118
|
+
const leader = Number(details.Leader);
|
|
119
|
+
let environment = {};
|
|
120
|
+
let start = '';
|
|
121
|
+
let unavailable = true;
|
|
122
|
+
if (details.Type === 'x11' && display && /^[1-9]\d*$/.test(details.Leader ?? '') && Number.isSafeInteger(leader)) {
|
|
123
|
+
try {
|
|
124
|
+
if ((await fs.stat(`/proc/${leader}`)).uid !== process.getuid?.())
|
|
125
|
+
throw new Error('Foreign graphical session leader');
|
|
126
|
+
start = await processStart(leader);
|
|
127
|
+
if (!/^\d+$/.test(start))
|
|
128
|
+
throw new Error('Invalid graphical session leader identity');
|
|
129
|
+
environment = await sessionEnvironment(leader).catch(() => ({}));
|
|
130
|
+
if (environment.DISPLAY && environment.DISPLAY.split('.')[0] !== display.split('.')[0])
|
|
131
|
+
throw new Error('Graphical session display mismatch');
|
|
132
|
+
if (environment.XDG_SESSION_ID && environment.XDG_SESSION_ID !== sessionId)
|
|
133
|
+
throw new Error('Graphical session identity mismatch');
|
|
134
|
+
environment.DISPLAY = display;
|
|
135
|
+
environment.XDG_SESSION_ID = sessionId;
|
|
136
|
+
environment.XDG_SESSION_TYPE = 'x11';
|
|
137
|
+
environment.XAUTHORITY ??= path.join(os.homedir(), '.Xauthority');
|
|
138
|
+
environment.XDG_RUNTIME_DIR ??= `/run/user/${process.getuid?.()}`;
|
|
139
|
+
unavailable = false;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
environment = {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const identity = createHash('sha256').update(`${sessionId}:${display}:${details.Leader ?? ''}:${start}`).digest('hex').slice(0, 32);
|
|
146
|
+
candidates.push({ identity, display, environment, managed: false, sessionId, processId: unavailable ? 0 : leader,
|
|
147
|
+
processStart: start, type: details.Type, locked: details.LockedHint === 'yes', unavailable });
|
|
148
|
+
}
|
|
149
|
+
return candidates;
|
|
150
|
+
}
|
|
151
|
+
const managedSchema = z.object({ identity: z.string().regex(/^[a-f0-9]{32}$/), display: z.string().regex(displayPattern),
|
|
152
|
+
environment: z.record(z.string(), z.string()), managed: z.literal(true), processId: z.number().int().positive(), processStart: z.string() });
|
|
153
|
+
async function readManaged(directory) {
|
|
154
|
+
try {
|
|
155
|
+
const file = await fs.lstat(path.join(directory, 'rd', 'managed.json'));
|
|
156
|
+
if (!file.isFile() || file.isSymbolicLink() || file.uid !== process.getuid?.() || file.mode & 0o077 || file.size > 16384)
|
|
157
|
+
throw new Error('Unsafe desktop session state');
|
|
158
|
+
const session = managedSchema.parse(JSON.parse(await fs.readFile(path.join(directory, 'rd', 'managed.json'), 'utf8')));
|
|
159
|
+
return await sessionAlive(session) ? session : undefined;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code === 'ENOENT')
|
|
163
|
+
return;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export async function ensureManagedSession(directory) {
|
|
168
|
+
const existing = await readManaged(directory);
|
|
169
|
+
if (existing)
|
|
170
|
+
return existing;
|
|
171
|
+
const cli = fileURLToPath(new URL('../../cli.js', import.meta.url));
|
|
172
|
+
const unit = `axom-rd-desktop-${createHash('sha256').update(directory).digest('hex').slice(0, 12)}`;
|
|
173
|
+
const args = [process.execPath, cli, '__rd-desktop', '--directory', directory];
|
|
174
|
+
try {
|
|
175
|
+
await execute('systemctl', ['--user', 'show-environment'], { timeout: 3000, maxBuffer: 65536 });
|
|
176
|
+
await execute('systemd-run', ['--user', `--unit=${unit}`, '--property=Restart=on-failure', '--property=RestartSec=3', '--property=KillMode=control-group', ...args], { timeout: 5000, maxBuffer: 8192 });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const recovered = await readManaged(directory);
|
|
180
|
+
if (recovered)
|
|
181
|
+
return recovered;
|
|
182
|
+
if (process.env.AXOM_DESKTOP_NATIVE_PROVIDER !== '1')
|
|
183
|
+
throw new Error(`A user service manager is required for a persistent virtual desktop: ${String(error)}`);
|
|
184
|
+
const child = spawn(args[0], args.slice(1), { detached: true, stdio: 'ignore', env: { ...process.env, AXOM_ACCESS_TOKEN: undefined } });
|
|
185
|
+
child.unref();
|
|
186
|
+
}
|
|
187
|
+
for (let attempt = 0; attempt < 350; attempt++) {
|
|
188
|
+
const session = await readManaged(directory);
|
|
189
|
+
if (session)
|
|
190
|
+
return session;
|
|
191
|
+
await delay(100);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`The virtual desktop did not become ready; inspect the ${unit} user service`);
|
|
194
|
+
}
|
|
195
|
+
function authFile(cookie) {
|
|
196
|
+
const pieces = [Buffer.from([255, 255])];
|
|
197
|
+
for (const value of [Buffer.alloc(0), Buffer.from('0'), Buffer.from('MIT-MAGIC-COOKIE-1'), cookie]) {
|
|
198
|
+
const length = Buffer.alloc(2);
|
|
199
|
+
length.writeUInt16BE(value.length);
|
|
200
|
+
pieces.push(length, value);
|
|
201
|
+
}
|
|
202
|
+
return Buffer.concat(pieces);
|
|
203
|
+
}
|
|
204
|
+
async function lineFrom(child) {
|
|
205
|
+
const stream = child.stdio[3];
|
|
206
|
+
if (!stream || !('on' in stream))
|
|
207
|
+
throw new Error('Desktop child did not supply a readiness pipe');
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
let text = '';
|
|
210
|
+
const timer = setTimeout(() => reject(new Error('Desktop readiness timed out')), 30000);
|
|
211
|
+
stream.on('data', (bytes) => {
|
|
212
|
+
text += bytes.toString();
|
|
213
|
+
if (text.length > 4096) {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
reject(new Error('Invalid desktop readiness'));
|
|
216
|
+
}
|
|
217
|
+
else if (text.includes('\n')) {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
resolve(text.split('\n')[0]);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
223
|
+
child.once('exit', () => { clearTimeout(timer); reject(new Error('Desktop child exited before readiness')); });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
export async function runManagedDesktop(directory) {
|
|
227
|
+
const state = await readRdState(directory);
|
|
228
|
+
if (!state?.enabled)
|
|
229
|
+
throw new Error('Remote desktop is disabled');
|
|
230
|
+
if (state.artifact.ipcVersion === 1)
|
|
231
|
+
return runLegacyManagedDesktop(directory);
|
|
232
|
+
await withPrivateLock(path.join(directory, 'rd', 'desktop-owner'), async () => {
|
|
233
|
+
const baseRuntime = process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid?.()}`;
|
|
234
|
+
const runtimeRoot = path.join(baseRuntime, 'axom-rd');
|
|
235
|
+
await privateDirectory(runtimeRoot);
|
|
236
|
+
const runtime = await fs.mkdtemp(path.join(runtimeRoot, 'desktop-'));
|
|
237
|
+
const profile = path.join(directory, 'rd', 'desktop');
|
|
238
|
+
await privateDirectory(profile);
|
|
239
|
+
const log = await fs.open(path.join(directory, 'rd', 'desktop.log'), 'a', 0o600);
|
|
240
|
+
let stopping = false;
|
|
241
|
+
const stop = () => { stopping = true; };
|
|
242
|
+
process.once('SIGTERM', stop);
|
|
243
|
+
process.once('SIGINT', stop);
|
|
244
|
+
const child = spawn(path.join(state.installation, 'bin/axom-desktop-session'), ['--managed', '--runtime', runtime,
|
|
245
|
+
'--profile', profile, '--xorg-config', path.join(state.installation, 'xorg.conf')], {
|
|
246
|
+
detached: true, stdio: ['ignore', log.fd, log.fd, 'pipe'],
|
|
247
|
+
env: { ...process.env, XDG_RUNTIME_DIR: baseRuntime, AXOM_DESKTOP_AUTO_DPI: '0', AXOM_ACCESS_TOKEN: undefined, SESSION_MANAGER: undefined },
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
const report = z.object({ display: z.string().regex(displayPattern), environment: z.record(z.string(), z.string()) }).parse(JSON.parse(await lineFrom(child)));
|
|
251
|
+
if (report.environment.DISPLAY !== report.display || report.environment.XAUTHORITY !== path.join(runtime, 'Xauthority'))
|
|
252
|
+
throw new Error('Invalid managed desktop environment');
|
|
253
|
+
const keys = [...allowedEnvironment, 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME'];
|
|
254
|
+
const environment = { ...Object.fromEntries(Object.entries(report.environment).filter(([key]) => keys.includes(key))),
|
|
255
|
+
AXOM_DESKTOP_AUTO_DPI: '0' };
|
|
256
|
+
const session = { identity: randomBytes(16).toString('hex'), display: report.display, environment, managed: true,
|
|
257
|
+
processId: process.pid, processStart: await processStart(process.pid) };
|
|
258
|
+
const staging = path.join(directory, 'rd', `.managed-${session.identity}.json`);
|
|
259
|
+
try {
|
|
260
|
+
await fs.writeFile(staging, JSON.stringify(session), { mode: 0o600, flag: 'wx' });
|
|
261
|
+
await fs.rename(staging, path.join(directory, 'rd', 'managed.json'));
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await fs.rm(staging, { force: true });
|
|
265
|
+
}
|
|
266
|
+
while (!stopping) {
|
|
267
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
268
|
+
throw new Error('The shared desktop runtime stopped');
|
|
269
|
+
await delay(250);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
await fs.rm(path.join(directory, 'rd', 'managed.json'), { force: true });
|
|
274
|
+
if (child.pid && child.exitCode === null && child.signalCode === null) {
|
|
275
|
+
try {
|
|
276
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
for (let attempt = 0; attempt < 70 && child.exitCode === null && child.signalCode === null; attempt++)
|
|
280
|
+
await delay(100);
|
|
281
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
282
|
+
try {
|
|
283
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
284
|
+
}
|
|
285
|
+
catch { }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
await log.close();
|
|
289
|
+
await fs.rm(runtime, { recursive: true, force: true });
|
|
290
|
+
process.removeListener('SIGTERM', stop);
|
|
291
|
+
process.removeListener('SIGINT', stop);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function runLegacyManagedDesktop(directory) {
|
|
296
|
+
await withPrivateLock(path.join(directory, 'rd', 'desktop-owner'), async () => {
|
|
297
|
+
const state = await readRdState(directory);
|
|
298
|
+
if (!state?.enabled)
|
|
299
|
+
throw new Error('Remote desktop is disabled');
|
|
300
|
+
const baseRuntime = process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid?.()}`;
|
|
301
|
+
const runtimeRoot = path.join(baseRuntime, 'axom-rd');
|
|
302
|
+
await privateDirectory(runtimeRoot);
|
|
303
|
+
const runtime = await fs.mkdtemp(path.join(runtimeRoot, 'desktop-'));
|
|
304
|
+
const profile = path.join(directory, 'rd', 'desktop');
|
|
305
|
+
for (const name of ['config', 'cache', 'data'])
|
|
306
|
+
await privateDirectory(path.join(profile, name));
|
|
307
|
+
const authority = path.join(runtime, 'Xauthority');
|
|
308
|
+
const cookie = randomBytes(16);
|
|
309
|
+
await fs.writeFile(authority, authFile(cookie), { mode: 0o600, flag: 'wx' });
|
|
310
|
+
const log = await fs.open(path.join(directory, 'rd', 'desktop.log'), 'a', 0o600);
|
|
311
|
+
const children = [];
|
|
312
|
+
let stopping = false;
|
|
313
|
+
const stop = () => { stopping = true; };
|
|
314
|
+
process.once('SIGTERM', stop);
|
|
315
|
+
process.once('SIGINT', stop);
|
|
316
|
+
try {
|
|
317
|
+
const xorg = spawn('/usr/lib/xorg/Xorg', ['-displayfd', '3', '-config', path.join(state.installation, 'xorg.conf'), '-auth', authority,
|
|
318
|
+
'-nolisten', 'tcp', '-noreset', '-logfile', path.join(runtime, 'Xorg.log')], { detached: true, stdio: ['ignore', log.fd, log.fd, 'pipe'] });
|
|
319
|
+
children.push(xorg);
|
|
320
|
+
const number = await lineFrom(xorg);
|
|
321
|
+
if (!/^\d+$/.test(number))
|
|
322
|
+
throw new Error('Invalid X server display number');
|
|
323
|
+
const display = `:${number}`;
|
|
324
|
+
const bus = spawn('dbus-daemon', ['--session', '--nofork', '--print-address=3'], { detached: true, stdio: ['ignore', log.fd, log.fd, 'pipe'] });
|
|
325
|
+
children.push(bus);
|
|
326
|
+
const busAddress = await lineFrom(bus);
|
|
327
|
+
const environment = { DISPLAY: display, XAUTHORITY: authority, XDG_RUNTIME_DIR: baseRuntime,
|
|
328
|
+
DBUS_SESSION_BUS_ADDRESS: busAddress, XDG_CURRENT_DESKTOP: 'XFCE', XDG_SESSION_TYPE: 'x11',
|
|
329
|
+
XDG_CONFIG_HOME: path.join(profile, 'config'), XDG_CACHE_HOME: path.join(profile, 'cache'), XDG_DATA_HOME: path.join(profile, 'data') };
|
|
330
|
+
const xauth = spawn('xauth', ['-f', authority, 'source', '-'], { stdio: ['pipe', log.fd, log.fd] });
|
|
331
|
+
xauth.stdin.end(`add ${display} . ${cookie.toString('hex')}\n`);
|
|
332
|
+
await new Promise((resolve, reject) => { xauth.once('error', reject); xauth.once('exit', code => code === 0 ? resolve() : reject(new Error('Xauthority setup failed'))); });
|
|
333
|
+
const desktop = spawn('xfce4-session', ['--disable-tcp'], { detached: true, stdio: ['ignore', log.fd, log.fd], env: { ...process.env, ...environment, SESSION_MANAGER: undefined, AXOM_ACCESS_TOKEN: undefined } });
|
|
334
|
+
children.push(desktop);
|
|
335
|
+
let ready = false;
|
|
336
|
+
for (let attempt = 0; attempt < 100 && !stopping; attempt++) {
|
|
337
|
+
if (children.some(child => child.exitCode !== null || child.signalCode !== null))
|
|
338
|
+
throw new Error('Desktop child exited during startup');
|
|
339
|
+
try {
|
|
340
|
+
const { stdout } = await execute('xprop', ['-root', '_NET_SUPPORTING_WM_CHECK'], { env: { ...process.env, ...environment }, timeout: 1000, maxBuffer: 4096 });
|
|
341
|
+
if (stdout.includes('window id')) {
|
|
342
|
+
ready = true;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch { }
|
|
347
|
+
await delay(100);
|
|
348
|
+
}
|
|
349
|
+
if (!ready)
|
|
350
|
+
throw new Error('Xfce did not become ready');
|
|
351
|
+
const session = { identity: randomBytes(16).toString('hex'), display, environment, managed: true,
|
|
352
|
+
processId: process.pid, processStart: await processStart(process.pid) };
|
|
353
|
+
const staging = path.join(directory, 'rd', `.managed-${session.identity}.json`);
|
|
354
|
+
try {
|
|
355
|
+
await fs.writeFile(staging, JSON.stringify(session), { mode: 0o600, flag: 'wx' });
|
|
356
|
+
await fs.rename(staging, path.join(directory, 'rd', 'managed.json'));
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
await fs.rm(staging, { force: true });
|
|
360
|
+
}
|
|
361
|
+
while (!stopping) {
|
|
362
|
+
if (children.some(child => child.exitCode !== null || child.signalCode !== null))
|
|
363
|
+
throw new Error('A virtual desktop component stopped');
|
|
364
|
+
await delay(250);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await fs.rm(path.join(directory, 'rd', 'managed.json'), { force: true });
|
|
369
|
+
for (const child of children.reverse()) {
|
|
370
|
+
if (child.pid && child.exitCode === null && child.signalCode === null) {
|
|
371
|
+
try {
|
|
372
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
373
|
+
}
|
|
374
|
+
catch { }
|
|
375
|
+
for (let attempt = 0; attempt < 20 && child.exitCode === null && child.signalCode === null; attempt++)
|
|
376
|
+
await delay(100);
|
|
377
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
378
|
+
try {
|
|
379
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await log.close();
|
|
386
|
+
await fs.rm(runtime, { recursive: true, force: true });
|
|
387
|
+
process.removeListener('SIGTERM', stop);
|
|
388
|
+
process.removeListener('SIGINT', stop);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { promises as fs, constants } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { rdArtifactSchema } from './artifact.js';
|
|
6
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
7
|
+
const selection = { schemaVersion: z.literal(2), artifactSource: z.string().url(),
|
|
8
|
+
session: z.enum(['auto', 'existing', 'virtual']), display: z.string().regex(/^:\d+(?:\.\d+)?$/).optional(),
|
|
9
|
+
};
|
|
10
|
+
const currentRdStateSchema = z.discriminatedUnion('enabled', [
|
|
11
|
+
z.strictObject({ ...selection, enabled: z.literal(true), installation: z.string().startsWith('/'), artifact: rdArtifactSchema }),
|
|
12
|
+
z.strictObject({ ...selection, enabled: z.literal(false), installation: z.string().startsWith('/').optional(), artifact: rdArtifactSchema.optional() }),
|
|
13
|
+
]);
|
|
14
|
+
const legacySelection = { schemaVersion: z.literal(1), server: z.string().url(), session: selection.session, display: selection.display };
|
|
15
|
+
const legacyRdStateSchema = z.discriminatedUnion('enabled', [
|
|
16
|
+
z.strictObject({ ...legacySelection, enabled: z.literal(true), installation: z.string().startsWith('/'), artifact: rdArtifactSchema }),
|
|
17
|
+
z.strictObject({ ...legacySelection, enabled: z.literal(false), installation: z.string().startsWith('/').optional(), artifact: rdArtifactSchema.optional() }),
|
|
18
|
+
]);
|
|
19
|
+
export const rdStateSchema = z.preprocess(value => {
|
|
20
|
+
const legacy = legacyRdStateSchema.safeParse(value);
|
|
21
|
+
if (!legacy.success)
|
|
22
|
+
return value;
|
|
23
|
+
const { server, ...state } = legacy.data;
|
|
24
|
+
return { ...state, schemaVersion: 2, artifactSource: server };
|
|
25
|
+
}, currentRdStateSchema);
|
|
26
|
+
export async function readRdState(directory) {
|
|
27
|
+
let file;
|
|
28
|
+
try {
|
|
29
|
+
file = await fs.open(path.join(directory, 'rd', 'state.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT')
|
|
33
|
+
return;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const stat = await file.stat();
|
|
38
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 32768)
|
|
39
|
+
throw new Error('Unsafe remote desktop state');
|
|
40
|
+
return rdStateSchema.parse(JSON.parse(await file.readFile('utf8')));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function writeRdState(directory, state) {
|
|
47
|
+
const root = path.join(directory, 'rd');
|
|
48
|
+
await privateDirectory(root);
|
|
49
|
+
const staging = path.join(root, `.state-${randomUUID()}`);
|
|
50
|
+
try {
|
|
51
|
+
await fs.writeFile(staging, JSON.stringify(rdStateSchema.parse(state)), { mode: 0o600, flag: 'wx' });
|
|
52
|
+
await fs.rename(staging, path.join(root, 'state.json'));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await fs.rm(staging, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { installRd, rdArtifactSchema, rdPlatform, verifyRdInstallation, RD_DEVICE_HOST_PLATFORM } from './artifact.js';
|
|
7
|
+
import { readRdState, writeRdState } from './state.js';
|
|
8
|
+
import { assertLockOwnership, withPrivateLock } from '../control/locks.js';
|
|
9
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { CLIENT_VERSION } from '../version.js';
|
|
12
|
+
import { authenticatedRdActivation, completeRdActivation, guardRdActivation, rdStateDigest, readPrivateRdFile } from '../update/rdRollback.js';
|
|
13
|
+
const execute = promisify(execFile);
|
|
14
|
+
async function selfTest(installation, artifact, options) {
|
|
15
|
+
if (options.selfTest)
|
|
16
|
+
return options.selfTest(installation);
|
|
17
|
+
const names = artifact.ipcVersion === 1 ? ['axom-desktop-keeper'] : ['axom-desktop-keeper', 'axom-display', 'axom-desktop-session'];
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
await (options.execute ?? execute)(path.join(installation, 'bin', name), ['--self-test'], {
|
|
20
|
+
timeout: 5000, maxBuffer: 8192, env: { ...process.env, AXOM_ACCESS_TOKEN: undefined },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function adoptBundledRd(directory, server, options = {}) {
|
|
25
|
+
await withPrivateLock(path.join(directory, 'rd', 'install'), () => adoptBundle(directory, server, options));
|
|
26
|
+
}
|
|
27
|
+
async function adoptBundle(directory, server, options) {
|
|
28
|
+
if (await readRdState(directory) !== undefined)
|
|
29
|
+
return;
|
|
30
|
+
const bundle = options.bundle ?? (process.env.AXOM_DESKTOP_BUNDLED_RD_ROOT && process.env.AXOM_DESKTOP_BUNDLED_RD_MANIFEST ? {
|
|
31
|
+
root: process.env.AXOM_DESKTOP_BUNDLED_RD_ROOT, manifest: process.env.AXOM_DESKTOP_BUNDLED_RD_MANIFEST, display: process.env.DISPLAY,
|
|
32
|
+
} : undefined);
|
|
33
|
+
if (!bundle)
|
|
34
|
+
return;
|
|
35
|
+
const catalog = z.object({ schemaVersion: z.literal(1), artifacts: z.record(z.string(), rdArtifactSchema) }).parse(JSON.parse(await fs.readFile(bundle.manifest, 'utf8')));
|
|
36
|
+
const artifact = catalog.artifacts[RD_DEVICE_HOST_PLATFORM];
|
|
37
|
+
if (!artifact || artifact.ipcVersion !== 2)
|
|
38
|
+
throw new Error('Bundled RD requires the device-host platform and IPC 2 runtime');
|
|
39
|
+
if (!bundle.display || !/^:\d+(?:\.\d+)?$/.test(bundle.display))
|
|
40
|
+
throw new Error('Bundled RD requires a local X11 DISPLAY');
|
|
41
|
+
if (!path.isAbsolute(bundle.root))
|
|
42
|
+
throw new Error('Bundled RD runtime root must be absolute');
|
|
43
|
+
await verifyRdInstallation(bundle.root, artifact);
|
|
44
|
+
await selfTest(bundle.root, artifact, options);
|
|
45
|
+
assertLockOwnership();
|
|
46
|
+
await writeRdState(directory, { schemaVersion: 2, enabled: true, artifactSource: server, session: 'existing', display: bundle.display, installation: bundle.root, artifact });
|
|
47
|
+
}
|
|
48
|
+
function requireSupportedIpc(artifact) {
|
|
49
|
+
if (artifact.ipcVersion !== 1 && artifact.ipcVersion !== 2)
|
|
50
|
+
throw new Error(`Unsupported RD IPC version ${artifact.ipcVersion}; this CLI supports versions 1 and 2`);
|
|
51
|
+
}
|
|
52
|
+
function reason(error) { return error instanceof Error ? error.message : String(error); }
|
|
53
|
+
async function writeStatus(directory, status) {
|
|
54
|
+
const root = path.join(directory, 'rd');
|
|
55
|
+
await privateDirectory(root);
|
|
56
|
+
const staging = path.join(root, `.update-status-${randomUUID()}`);
|
|
57
|
+
try {
|
|
58
|
+
await fs.writeFile(staging, JSON.stringify({ schemaVersion: 1, updatedAt: new Date().toISOString(), ...status }), { flag: 'wx', mode: 0o600 });
|
|
59
|
+
assertLockOwnership();
|
|
60
|
+
await fs.rename(staging, path.join(root, 'update-status.json'));
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await fs.rm(staging, { force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function synchronizeRd(directory, server, options = {}) {
|
|
67
|
+
return withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
|
|
68
|
+
let previous;
|
|
69
|
+
try {
|
|
70
|
+
await adoptBundle(directory, server, options);
|
|
71
|
+
previous = await readRdState(directory);
|
|
72
|
+
if (!previous?.enabled)
|
|
73
|
+
return;
|
|
74
|
+
requireSupportedIpc(previous.artifact);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
await writeStatus(directory, { state: 'blocked', reason: reason(error) });
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const fallback = async (error) => {
|
|
81
|
+
try {
|
|
82
|
+
requireSupportedIpc(previous.artifact);
|
|
83
|
+
await verifyRdInstallation(previous.installation, previous.artifact);
|
|
84
|
+
await selfTest(previous.installation, previous.artifact, options);
|
|
85
|
+
}
|
|
86
|
+
catch (fallbackError) {
|
|
87
|
+
await writeStatus(directory, { state: 'blocked', reason: reason(error), fallbackReason: reason(fallbackError), previousVersion: previous.artifact.version });
|
|
88
|
+
throw new Error(`RD synchronization failed: ${reason(error)}; previous installation is unusable: ${reason(fallbackError)}`, { cause: fallbackError });
|
|
89
|
+
}
|
|
90
|
+
await writeStatus(directory, { state: 'fallback', activeVersion: previous.artifact.version, reason: reason(error) });
|
|
91
|
+
};
|
|
92
|
+
let installed;
|
|
93
|
+
try {
|
|
94
|
+
const platform = options.platform ?? rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
|
|
95
|
+
installed = await (options.install ?? installRd)(directory, previous.artifactSource, platform, options.fetch ?? globalThis.fetch);
|
|
96
|
+
requireSupportedIpc(installed.artifact);
|
|
97
|
+
await verifyRdInstallation(installed.directory, installed.artifact);
|
|
98
|
+
await selfTest(installed.directory, installed.artifact, options);
|
|
99
|
+
const next = { ...previous, installation: installed.directory, artifact: installed.artifact };
|
|
100
|
+
if (options.deferActivation) {
|
|
101
|
+
if (installed.artifact.sha512 === previous.artifact.sha512) {
|
|
102
|
+
await writeStatus(directory, { state: 'current', activeVersion: previous.artifact.version });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await writeStatus(directory, { state: 'staged', activeVersion: previous.artifact.version, targetVersion: installed.artifact.version,
|
|
106
|
+
reason: 'RD activation waits for an idle remote desktop' });
|
|
107
|
+
const candidate = installed;
|
|
108
|
+
const expected = JSON.stringify(previous);
|
|
109
|
+
return async () => withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
|
|
110
|
+
if (JSON.stringify(await readRdState(directory)) !== expected)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
await verifyRdInstallation(candidate.directory, candidate.artifact);
|
|
114
|
+
await selfTest(candidate.directory, candidate.artifact, options);
|
|
115
|
+
if (await guardRdActivation(directory, options.cliVersion ?? CLIENT_VERSION, next))
|
|
116
|
+
throw new Error('RD activation waits for authenticated CLI update readiness');
|
|
117
|
+
assertLockOwnership();
|
|
118
|
+
await writeRdState(directory, next);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
await fallback(error);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
await completeRdActivation(directory, options.cliVersion ?? CLIENT_VERSION);
|
|
125
|
+
await writeStatus(directory, { state: 'updated', activeVersion: candidate.artifact.version, previousVersion: previous.artifact.version,
|
|
126
|
+
previousInstallation: previous.installation, previousArtifact: previous.artifact });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const guard = await guardRdActivation(directory, options.cliVersion ?? CLIENT_VERSION, next);
|
|
130
|
+
if (guard) {
|
|
131
|
+
await verifyRdInstallation(previous.installation, previous.artifact);
|
|
132
|
+
await selfTest(previous.installation, previous.artifact, options);
|
|
133
|
+
await writeStatus(directory, { state: 'staged', activeVersion: previous.artifact.version, targetVersion: installed.artifact.version,
|
|
134
|
+
reason: 'RD activation waits for authenticated CLI readiness because this watchdog cannot restore the previous RD state' });
|
|
135
|
+
const candidate = installed;
|
|
136
|
+
return async () => withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
|
|
137
|
+
if (!await authenticatedRdActivation(directory, guard))
|
|
138
|
+
return;
|
|
139
|
+
const active = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
|
|
140
|
+
if (active === null || rdStateDigest(active) !== guard.previousSha256)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
await verifyRdInstallation(candidate.directory, candidate.artifact);
|
|
144
|
+
await selfTest(candidate.directory, candidate.artifact, options);
|
|
145
|
+
assertLockOwnership();
|
|
146
|
+
await writeRdState(directory, next);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
await fallback(error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
await writeStatus(directory, { state: 'updated', activeVersion: candidate.artifact.version, previousVersion: previous.artifact.version,
|
|
153
|
+
previousInstallation: previous.installation, previousArtifact: previous.artifact });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
assertLockOwnership();
|
|
157
|
+
await writeRdState(directory, next);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await fallback(error);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await completeRdActivation(directory, options.cliVersion ?? CLIENT_VERSION);
|
|
164
|
+
await writeStatus(directory, { state: installed.artifact.sha512 === previous.artifact.sha512 ? 'current' : 'updated',
|
|
165
|
+
activeVersion: installed.artifact.version, previousVersion: previous.artifact.version, previousInstallation: previous.installation,
|
|
166
|
+
previousArtifact: previous.artifact });
|
|
167
|
+
});
|
|
168
|
+
}
|