tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26
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/bin/cli.js +46791 -43589
- package/package.json +2 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Server, IncomingMessage } from "node:http";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmuxServerPaneStreamPath } from "@tmux-ide/contracts";
|
|
6
|
+
import {
|
|
7
|
+
TmuxServerOwners,
|
|
8
|
+
MAX_TMUX_SERVER_OWNERS,
|
|
9
|
+
type TmuxServerRegistration,
|
|
10
|
+
} from "./tmux-server-owners.ts";
|
|
11
|
+
import {
|
|
12
|
+
createTmuxServerProbe,
|
|
13
|
+
readTmuxServerRegistrations,
|
|
14
|
+
writeTmuxServerRegistrations,
|
|
15
|
+
} from "./tmux-server-registration.ts";
|
|
16
|
+
import { createNativeTmuxServerOwner, type NativeTmuxServerOwner } from "./tmux-server-owner.ts";
|
|
17
|
+
import type { WorkspacePaneTmuxAuthority } from "./workspace-pane-creation.ts";
|
|
18
|
+
import {
|
|
19
|
+
attachPaneStreamWebSocket,
|
|
20
|
+
type PaneStreamWebSocketBoundary,
|
|
21
|
+
} from "../server/pane-stream-upgrade.ts";
|
|
22
|
+
|
|
23
|
+
/** Adapts the established default owner without duplicating its control clients. */
|
|
24
|
+
export async function createEmbeddedTmuxServerOwners(options: {
|
|
25
|
+
readonly defaultAuthority: WorkspacePaneTmuxAuthority;
|
|
26
|
+
readonly defaultGeneration: string;
|
|
27
|
+
readonly expectedDefaultProofDigest: string | null;
|
|
28
|
+
readonly defaultOwner: Omit<NativeTmuxServerOwner, "serverId" | "generation">;
|
|
29
|
+
readonly stateDirectory: string;
|
|
30
|
+
readonly webSocketBaseUrl: string;
|
|
31
|
+
}) {
|
|
32
|
+
const path = join(options.stateDirectory, "tmux-servers.json");
|
|
33
|
+
const persisted = readTmuxServerRegistrations(path);
|
|
34
|
+
const probe = createTmuxServerProbe(options.defaultAuthority.executablePath);
|
|
35
|
+
const defaultObservation = await probe(options.defaultAuthority.socketSelector);
|
|
36
|
+
let established = persisted.find(
|
|
37
|
+
(entry) =>
|
|
38
|
+
JSON.stringify(entry.selector) === JSON.stringify(options.defaultAuthority.socketSelector),
|
|
39
|
+
);
|
|
40
|
+
if (!established && defaultObservation) {
|
|
41
|
+
for (const entry of persisted) {
|
|
42
|
+
const observation = await probe(entry.selector);
|
|
43
|
+
if (observation?.fingerprint === defaultObservation.fingerprint) {
|
|
44
|
+
established = entry;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const defaultRegistration: TmuxServerRegistration = established ?? {
|
|
50
|
+
serverId: `tmux-server.${randomUUID().replaceAll("-", "")}`,
|
|
51
|
+
label: "Default",
|
|
52
|
+
selector: options.defaultAuthority.socketSelector,
|
|
53
|
+
};
|
|
54
|
+
let httpServer: Server | null = null;
|
|
55
|
+
const boundaries = new Map<string, PaneStreamWebSocketBoundary>();
|
|
56
|
+
const pending = new Map<string, NativeTmuxServerOwner>();
|
|
57
|
+
const isCurrent = (owner: NativeTmuxServerOwner): boolean => {
|
|
58
|
+
try {
|
|
59
|
+
return (
|
|
60
|
+
owners.current({ serverId: owner.serverId, generation: owner.generation })
|
|
61
|
+
.paneStreamRuntime === owner.paneStreamRuntime
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const attachOwner = (route: string, owner: NativeTmuxServerOwner) => {
|
|
68
|
+
pending.set(route, owner);
|
|
69
|
+
if (httpServer)
|
|
70
|
+
boundaries.set(
|
|
71
|
+
route,
|
|
72
|
+
attachPaneStreamWebSocket(httpServer, owner.paneStreamRuntime.coordinator, route, () =>
|
|
73
|
+
isCurrent(owner),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
let defaultRetired = false;
|
|
78
|
+
const retireDefault = async () => {
|
|
79
|
+
if (defaultRetired) return;
|
|
80
|
+
defaultRetired = true;
|
|
81
|
+
await options.defaultOwner.dispose();
|
|
82
|
+
};
|
|
83
|
+
const owners = new TmuxServerOwners<NativeTmuxServerOwner>({
|
|
84
|
+
probe,
|
|
85
|
+
persist: (registrations) => writeTmuxServerRegistrations(path, registrations),
|
|
86
|
+
create: async (registration, scope, observation) => {
|
|
87
|
+
if (registration.serverId === defaultRegistration.serverId) await retireDefault();
|
|
88
|
+
const route = tmuxServerPaneStreamPath(scope);
|
|
89
|
+
const owner = await createNativeTmuxServerOwner({
|
|
90
|
+
...scope,
|
|
91
|
+
tmuxAuthority: observation.authority,
|
|
92
|
+
nativeServerIdentity: observation.nativeServerIdentity,
|
|
93
|
+
stateDirectory: join(options.stateDirectory, "server-owners", registration.serverId),
|
|
94
|
+
webSocketUrl: new URL(route, options.webSocketBaseUrl).href,
|
|
95
|
+
});
|
|
96
|
+
attachOwner(route, owner);
|
|
97
|
+
return {
|
|
98
|
+
...owner,
|
|
99
|
+
dispose: async () => {
|
|
100
|
+
const boundary = boundaries.get(route);
|
|
101
|
+
boundaries.delete(route);
|
|
102
|
+
pending.delete(route);
|
|
103
|
+
try {
|
|
104
|
+
await boundary?.close();
|
|
105
|
+
} finally {
|
|
106
|
+
await owner.dispose();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
if (defaultObservation && defaultObservation.fingerprint === options.expectedDefaultProofDigest)
|
|
113
|
+
owners.adopt(
|
|
114
|
+
defaultRegistration,
|
|
115
|
+
{ serverId: defaultRegistration.serverId, generation: options.defaultGeneration },
|
|
116
|
+
defaultObservation,
|
|
117
|
+
{
|
|
118
|
+
...options.defaultOwner,
|
|
119
|
+
serverId: defaultRegistration.serverId,
|
|
120
|
+
generation: options.defaultGeneration,
|
|
121
|
+
dispose: retireDefault,
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
try {
|
|
125
|
+
if (
|
|
126
|
+
!defaultObservation ||
|
|
127
|
+
defaultObservation.fingerprint !== options.expectedDefaultProofDigest
|
|
128
|
+
)
|
|
129
|
+
await owners.register(defaultRegistration);
|
|
130
|
+
for (const registration of persisted) {
|
|
131
|
+
if (registration.serverId !== defaultRegistration.serverId)
|
|
132
|
+
await owners.register(registration);
|
|
133
|
+
}
|
|
134
|
+
writeTmuxServerRegistrations(path, owners.registrations());
|
|
135
|
+
} catch (error) {
|
|
136
|
+
await owners.dispose();
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const refuseUnknownScope = (request: IncomingMessage, socket: Socket) => {
|
|
140
|
+
const route = (request.url ?? "").split("?", 1)[0]!;
|
|
141
|
+
if (route.startsWith("/v2/tmux-servers/") && !pending.has(route))
|
|
142
|
+
socket.end("HTTP/1.1 410 Gone\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
owners,
|
|
146
|
+
isDefaultCurrent() {
|
|
147
|
+
if (defaultRetired) return false;
|
|
148
|
+
if (
|
|
149
|
+
!defaultObservation ||
|
|
150
|
+
defaultObservation.fingerprint !== options.expectedDefaultProofDigest
|
|
151
|
+
)
|
|
152
|
+
return true;
|
|
153
|
+
try {
|
|
154
|
+
owners.current({
|
|
155
|
+
serverId: defaultRegistration.serverId,
|
|
156
|
+
generation: options.defaultGeneration,
|
|
157
|
+
});
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
attach(server: Server) {
|
|
164
|
+
if (httpServer) throw new Error("Server owner transport already attached");
|
|
165
|
+
httpServer = server;
|
|
166
|
+
server.on("upgrade", refuseUnknownScope);
|
|
167
|
+
server.setMaxListeners(
|
|
168
|
+
Math.max(
|
|
169
|
+
server.getMaxListeners(),
|
|
170
|
+
MAX_TMUX_SERVER_OWNERS + server.listenerCount("upgrade") + 4,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
for (const [route, owner] of pending)
|
|
174
|
+
boundaries.set(
|
|
175
|
+
route,
|
|
176
|
+
attachPaneStreamWebSocket(server, owner.paneStreamRuntime.coordinator, route, () =>
|
|
177
|
+
isCurrent(owner),
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
async dispose() {
|
|
182
|
+
httpServer?.off("upgrade", refuseUnknownScope);
|
|
183
|
+
await owners.dispose();
|
|
184
|
+
},
|
|
185
|
+
/** Legacy default routes remain bound to their original authority; never retarget. */
|
|
186
|
+
get defaultRetired() {
|
|
187
|
+
return defaultRetired;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.ts";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { realpath, stat } from "node:fs/promises";
|
|
@@ -56,6 +57,7 @@ export class FleetLifecycleAuthority {
|
|
|
56
57
|
readonly #startedAt: string;
|
|
57
58
|
readonly #registry: Pick<WorkspaceRegistry, "list" | "add">;
|
|
58
59
|
readonly #runTmux: (args: readonly string[]) => string;
|
|
60
|
+
readonly #ensureChromeUpdater: boolean;
|
|
59
61
|
readonly #readFleet: () => FleetSessionFacts[] | null;
|
|
60
62
|
readonly #operations = new Map<string, { fingerprint: string; result: unknown }>();
|
|
61
63
|
#tail: Promise<void> = Promise.resolve();
|
|
@@ -67,11 +69,14 @@ export class FleetLifecycleAuthority {
|
|
|
67
69
|
registry: Pick<WorkspaceRegistry, "list" | "add">;
|
|
68
70
|
runTmux: (args: readonly string[]) => string;
|
|
69
71
|
readFleet?: () => FleetSessionFacts[] | null;
|
|
72
|
+
/** Independent visual owners do not need an additional chrome updater process. */
|
|
73
|
+
ensureChromeUpdater?: boolean;
|
|
70
74
|
}) {
|
|
71
75
|
this.#daemonInstanceId = options.daemonInstanceId;
|
|
72
76
|
this.#productVersion = options.productVersion;
|
|
73
77
|
this.#startedAt = options.startedAt;
|
|
74
78
|
this.#registry = options.registry;
|
|
79
|
+
this.#ensureChromeUpdater = options.ensureChromeUpdater ?? true;
|
|
75
80
|
this.#runTmux = options.runTmux;
|
|
76
81
|
this.#readFleet = options.readFleet ?? (() => readAdoptedFleet(this.#registry));
|
|
77
82
|
}
|
|
@@ -135,10 +140,14 @@ export class FleetLifecycleAuthority {
|
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
let created = false;
|
|
143
|
+
let liveSessionId: string | undefined;
|
|
138
144
|
try {
|
|
139
|
-
this.#runTmux([
|
|
145
|
+
const createdId = this.#runTmux([
|
|
140
146
|
"new-session",
|
|
141
147
|
"-d",
|
|
148
|
+
"-P",
|
|
149
|
+
"-F",
|
|
150
|
+
"#{pid}\t#{session_id}\t#{session_created}",
|
|
142
151
|
...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
|
|
143
152
|
"-s",
|
|
144
153
|
identity.sessionName,
|
|
@@ -148,14 +157,26 @@ export class FleetLifecycleAuthority {
|
|
|
148
157
|
cwd,
|
|
149
158
|
TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND,
|
|
150
159
|
]);
|
|
160
|
+
const [serverPid, sessionId, sessionCreated] = createdId.trim().split("\t");
|
|
161
|
+
if (
|
|
162
|
+
serverPid &&
|
|
163
|
+
/^\d+$/u.test(serverPid) &&
|
|
164
|
+
sessionId &&
|
|
165
|
+
/^\$\d+$/u.test(sessionId) &&
|
|
166
|
+
sessionCreated &&
|
|
167
|
+
/^\d+$/u.test(sessionCreated)
|
|
168
|
+
)
|
|
169
|
+
liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
|
|
151
170
|
created = true;
|
|
152
171
|
prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
|
|
153
172
|
this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
|
|
154
173
|
this.#runTmux(adoptMarkArgv(identity.sessionName));
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
if (this.#ensureChromeUpdater) {
|
|
175
|
+
try {
|
|
176
|
+
this.#runTmux(updaterProbeArgv());
|
|
177
|
+
} catch {
|
|
178
|
+
this.#runTmux(updaterSpawnArgv());
|
|
179
|
+
}
|
|
159
180
|
}
|
|
160
181
|
if (!existing)
|
|
161
182
|
this.#registry.add({
|
|
@@ -180,6 +201,7 @@ export class FleetLifecycleAuthority {
|
|
|
180
201
|
operationId,
|
|
181
202
|
daemonInstanceId: this.#daemonInstanceId,
|
|
182
203
|
outcome: "created",
|
|
204
|
+
...(input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {}),
|
|
183
205
|
fleetSessionId: fleetSessionIdForName(identity.sessionName),
|
|
184
206
|
workspaceName: identity.workspaceName,
|
|
185
207
|
displayName: input.displayName,
|
|
@@ -368,10 +390,12 @@ export class FleetLifecycleAuthority {
|
|
|
368
390
|
this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_launch", input.command]);
|
|
369
391
|
this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", input.harness]);
|
|
370
392
|
this.#runTmux(["select-pane", "-t", paneId, "-T", input.displayTitle]);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
if (this.#ensureChromeUpdater) {
|
|
394
|
+
try {
|
|
395
|
+
this.#runTmux(updaterProbeArgv());
|
|
396
|
+
} catch {
|
|
397
|
+
this.#runTmux(updaterSpawnArgv());
|
|
398
|
+
}
|
|
375
399
|
}
|
|
376
400
|
} catch (error) {
|
|
377
401
|
if (createdSession) this.#tryTmux(["kill-session", "-t", sessionName]);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertCanonicalDaemonServerIntent } from "./canonical-daemon-bootstrap.ts";
|
|
2
|
+
import { resolveTmuxServerIntent } from "./runtime-namespace.ts";
|
|
1
3
|
import {
|
|
2
4
|
canonicalDaemonUrl,
|
|
3
5
|
matchesCanonicalDaemonPredecessor,
|
|
@@ -28,6 +30,7 @@ import { generateAuthToken } from "./auth-token.ts";
|
|
|
28
30
|
import type { DaemonRestartRequest } from "./daemon-restart-request.ts";
|
|
29
31
|
|
|
30
32
|
export interface HeadlessDaemonOptions {
|
|
33
|
+
readonly tmuxServerIntent?: ReturnType<typeof resolveTmuxServerIntent>;
|
|
31
34
|
readonly supervisionId?: string;
|
|
32
35
|
/** Managed launch receipt, only after this process wins election and is attachable. */
|
|
33
36
|
readonly onOwnedReady?: (info: CanonicalDaemonInfo) => void;
|
|
@@ -48,6 +51,8 @@ export interface HeadlessDaemonDependencies {
|
|
|
48
51
|
readonly probeCanonicalDaemonHealth: (info: CanonicalDaemonInfo) => Promise<DaemonHealth | null>;
|
|
49
52
|
readonly probeCanonicalDaemonIdentity: (
|
|
50
53
|
info: CanonicalDaemonInfo,
|
|
54
|
+
signal?: AbortSignal,
|
|
55
|
+
includeTmuxServerProof?: boolean,
|
|
51
56
|
) => Promise<DaemonIdentity | null>;
|
|
52
57
|
readonly startEmbeddedDaemon: (opts: EmbeddedDaemonOptions) => Promise<EmbeddedDaemonHandle>;
|
|
53
58
|
readonly writeStdout: (line: string) => void;
|
|
@@ -140,7 +145,11 @@ async function assertAttachableDaemon(
|
|
|
140
145
|
info: CanonicalDaemonInfo,
|
|
141
146
|
options: HeadlessDaemonOptions,
|
|
142
147
|
): Promise<void> {
|
|
143
|
-
const identity = await deps.probeCanonicalDaemonIdentity(
|
|
148
|
+
const identity = await deps.probeCanonicalDaemonIdentity(
|
|
149
|
+
info,
|
|
150
|
+
undefined,
|
|
151
|
+
Boolean(options.tmuxServerIntent),
|
|
152
|
+
);
|
|
144
153
|
if (!identity) {
|
|
145
154
|
throw new IdeError(
|
|
146
155
|
`Canonical daemon PID ${info.pid} is alive but its identity endpoint is unavailable. ` +
|
|
@@ -149,6 +158,9 @@ async function assertAttachableDaemon(
|
|
|
149
158
|
);
|
|
150
159
|
}
|
|
151
160
|
assertIdentityMatches(info, identity);
|
|
161
|
+
await assertCanonicalDaemonServerIntent(info, options.tmuxServerIntent ?? null, {
|
|
162
|
+
identity: async () => identity,
|
|
163
|
+
});
|
|
152
164
|
const health = await deps.probeCanonicalDaemonHealth(info);
|
|
153
165
|
if (!health) {
|
|
154
166
|
throw new IdeError(
|
|
@@ -288,6 +300,11 @@ export async function runHeadlessDaemon(
|
|
|
288
300
|
options: HeadlessDaemonOptions = {},
|
|
289
301
|
deps: HeadlessDaemonDependencies = defaultDependencies,
|
|
290
302
|
): Promise<"stopped" | "already-running"> {
|
|
303
|
+
options = {
|
|
304
|
+
...options,
|
|
305
|
+
tmuxServerIntent:
|
|
306
|
+
options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
|
|
307
|
+
};
|
|
291
308
|
let restoreTmuxWorkspaces = false;
|
|
292
309
|
const lifecycle: { restart?: DaemonRestartRequest; predecessor?: CanonicalDaemonPredecessor } =
|
|
293
310
|
{};
|
|
@@ -44,6 +44,7 @@ export interface RuntimeNamespace {
|
|
|
44
44
|
readonly daemonInfoDir: string;
|
|
45
45
|
readonly controlSocketPath: string;
|
|
46
46
|
readonly eventLogPath: string;
|
|
47
|
+
readonly tmuxSocketExplicit?: boolean;
|
|
47
48
|
readonly tmuxSocket:
|
|
48
49
|
| { readonly kind: "name"; readonly name: string }
|
|
49
50
|
| { readonly kind: "path"; readonly path: string };
|
|
@@ -287,6 +288,7 @@ export function resolveRuntimeNamespace(
|
|
|
287
288
|
controlSocketPath: join(runtimeDir, "control.sock"),
|
|
288
289
|
eventLogPath: join(stateHome, "events.jsonl"),
|
|
289
290
|
tmuxSocket,
|
|
291
|
+
tmuxSocketExplicit: Boolean(tmuxSocketName || tmuxSocketPath),
|
|
290
292
|
cleanupToken,
|
|
291
293
|
namespaceId: development?.id ?? (isolated ? cleanupToken! : "canonical"),
|
|
292
294
|
persistence: isolated && !development ? "ephemeral" : "durable",
|
|
@@ -316,9 +318,11 @@ export function runtimeNamespaceEnvironment(
|
|
|
316
318
|
[STATE_HOME_ENV]: namespace.stateHome,
|
|
317
319
|
[REGISTRY_DIR_ENV]: namespace.registryDir,
|
|
318
320
|
[DAEMON_INFO_DIR_ENV]: namespace.daemonInfoDir,
|
|
319
|
-
...(namespace.
|
|
320
|
-
? {
|
|
321
|
-
:
|
|
321
|
+
...(namespace.tmuxSocketExplicit === false
|
|
322
|
+
? {}
|
|
323
|
+
: namespace.tmuxSocket.kind === "name"
|
|
324
|
+
? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
|
|
325
|
+
: { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
|
|
322
326
|
...(namespace.cleanupToken ? { [CLEANUP_TOKEN_ENV]: namespace.cleanupToken } : {}),
|
|
323
327
|
});
|
|
324
328
|
}
|
|
@@ -388,3 +392,20 @@ export function assertQualifiedDevelopmentLaunch(): void {
|
|
|
388
392
|
"Development launch requires exact build artifacts (D03); installed CLI fallback is disabled",
|
|
389
393
|
);
|
|
390
394
|
}
|
|
395
|
+
|
|
396
|
+
/** Caller intent differs from the default used when starting a new owner. */
|
|
397
|
+
export function resolveTmuxServerIntent(options: RuntimeNamespaceResolutionOptions = {}): {
|
|
398
|
+
readonly source: "explicit" | "current";
|
|
399
|
+
readonly selector: RuntimeNamespace["tmuxSocket"];
|
|
400
|
+
} | null {
|
|
401
|
+
const env = options.env ?? process.env;
|
|
402
|
+
const namespace = resolveRuntimeNamespace(options);
|
|
403
|
+
if (
|
|
404
|
+
namespace.isolated ||
|
|
405
|
+
nonEmpty(env, TMUX_SOCKET_NAME_ENV) ||
|
|
406
|
+
nonEmpty(env, TMUX_SOCKET_PATH_ENV)
|
|
407
|
+
)
|
|
408
|
+
return { source: "explicit", selector: namespace.tmuxSocket };
|
|
409
|
+
const current = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX ?? "")?.[1];
|
|
410
|
+
return current ? { source: "current", selector: { kind: "path", path: current } } : null;
|
|
411
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
+
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
4
|
+
import { resolveBundledTmux } from "./bundled-tmux.ts";
|
|
5
|
+
|
|
6
|
+
export function resolveTmuxExecutable(): string {
|
|
7
|
+
if (resolveRuntimeNamespace().development) return resolveBundledTmux()!;
|
|
8
|
+
const configured = process.env.TMUX_IDE_TMUX_BIN;
|
|
9
|
+
if (!configured) {
|
|
10
|
+
const bundled = resolveBundledTmux();
|
|
11
|
+
if (bundled) return bundled;
|
|
12
|
+
}
|
|
13
|
+
const candidates = configured
|
|
14
|
+
? [configured]
|
|
15
|
+
: (process.env.PATH ?? "")
|
|
16
|
+
.split(delimiter)
|
|
17
|
+
// Empty and relative PATH entries mean the daemon/project cwd. They
|
|
18
|
+
// are never executable authority for a privileged tmux mutation.
|
|
19
|
+
.filter((entry) => entry.length > 0 && isAbsolute(entry))
|
|
20
|
+
.map((entry) => join(entry, "tmux"));
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
if (!isAbsolute(candidate)) continue;
|
|
24
|
+
accessSync(candidate, constants.X_OK);
|
|
25
|
+
const canonical = realpathSync(candidate);
|
|
26
|
+
if (statSync(canonical).isFile()) return canonical;
|
|
27
|
+
} catch {
|
|
28
|
+
// Continue to the next daemon-start candidate.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error("tmux_executable_unavailable");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
|
|
35
|
+
const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The pinned tmux client receives only presentation metadata. Everything else
|
|
39
|
+
* is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
|
|
40
|
+
* startup hooks, dynamic-loader variables, Node options, and project secrets
|
|
41
|
+
* cannot redirect authority or inject code into this daemon-owned execution.
|
|
42
|
+
*/
|
|
43
|
+
export function tmuxClientEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
44
|
+
const environment: NodeJS.ProcessEnv = {
|
|
45
|
+
TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
|
|
46
|
+
// A pinned runner may be the first tmux client and therefore create the
|
|
47
|
+
// server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
|
|
48
|
+
// the global environment inherited by every later interactive child.
|
|
49
|
+
COLORTERM: "truecolor",
|
|
50
|
+
};
|
|
51
|
+
for (const name of ["LANG", "LC_ALL", "LC_CTYPE"] as const) {
|
|
52
|
+
const value = source[name];
|
|
53
|
+
if (value && SAFE_LOCALE_VALUE.test(value)) environment[name] = value;
|
|
54
|
+
}
|
|
55
|
+
return environment;
|
|
56
|
+
}
|
|
@@ -68,3 +68,8 @@ export function createNamedSocketFence(
|
|
|
68
68
|
isPinned: () => shared.identity !== null,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/** True only before this exact authority has observed any live socket. */
|
|
73
|
+
export function isNamedSocketAuthorityUnbound(authority: WorkspacePaneTmuxAuthority): boolean {
|
|
74
|
+
return authority.socketSelector.kind === "name" && !states.get(authority)?.identity;
|
|
75
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
createPinnedWorkspaceTmuxRunner,
|
|
4
|
+
createPinnedWorkspaceTmuxAsyncRunner,
|
|
5
|
+
type WorkspacePaneTmuxAuthority,
|
|
6
|
+
} from "./workspace-pane-creation.ts";
|
|
7
|
+
import { shellEscape } from "./shell.ts";
|
|
8
|
+
|
|
9
|
+
export interface NativeTmuxServerIdentity {
|
|
10
|
+
readonly pid: string;
|
|
11
|
+
readonly startTime: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Guard each command on the server which actually accepted this connection. */
|
|
15
|
+
export function fenceNativeTmuxCommand(
|
|
16
|
+
args: readonly string[],
|
|
17
|
+
expected: NativeTmuxServerIdentity,
|
|
18
|
+
) {
|
|
19
|
+
const { pid, startTime } = expected;
|
|
20
|
+
if (!/^[1-9][0-9]*$/u.test(pid) || !/^[1-9][0-9]*$/u.test(startTime))
|
|
21
|
+
throw new Error("Invalid native tmux server identity");
|
|
22
|
+
if (args.length === 0 || args.some((arg) => arg.includes("\0")))
|
|
23
|
+
throw new Error("Invalid tmux command");
|
|
24
|
+
const globalFlags: string[] = [];
|
|
25
|
+
let firstCommand = 0;
|
|
26
|
+
while (args[firstCommand]?.startsWith("-")) {
|
|
27
|
+
const flag = args[firstCommand]!;
|
|
28
|
+
if (flag !== "-N" && flag !== "-u")
|
|
29
|
+
throw new Error("Unsupported tmux global option in generation-fenced command");
|
|
30
|
+
globalFlags.push(flag);
|
|
31
|
+
firstCommand += 1;
|
|
32
|
+
}
|
|
33
|
+
if (firstCommand === args.length) throw new Error("Missing tmux command");
|
|
34
|
+
const condition = `#{&&:#{==:#{pid},${pid}},#{==:#{start_time},${startTime}}}`;
|
|
35
|
+
// Preserve arbitrary literal payloads; a standalone ; is the native argv
|
|
36
|
+
// command separator already used by the existing runners.
|
|
37
|
+
const command = args
|
|
38
|
+
.slice(firstCommand)
|
|
39
|
+
.map((arg) => (arg === ";" ? ";" : shellEscape(arg)))
|
|
40
|
+
.join(" ");
|
|
41
|
+
const refusal = `tmux-server-stale.${randomUUID()}`;
|
|
42
|
+
return {
|
|
43
|
+
argv: [...globalFlags, "if-shell", "-F", condition, command, `display-message -p '${refusal}'`],
|
|
44
|
+
verify(output: string): string {
|
|
45
|
+
if (output.trimEnd() === refusal)
|
|
46
|
+
throw new Error("Tmux server generation changed before command execution");
|
|
47
|
+
return output;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Socket validation alone leaves a validation-to-connect replacement race. */
|
|
53
|
+
export function createServerGenerationFencedTmuxRunner(
|
|
54
|
+
authority: WorkspacePaneTmuxAuthority,
|
|
55
|
+
expected?: NativeTmuxServerIdentity,
|
|
56
|
+
): (args: readonly string[]) => string {
|
|
57
|
+
const run = createPinnedWorkspaceTmuxRunner(authority, { timeoutMs: 5_000 });
|
|
58
|
+
const [pid, startTime] = expected
|
|
59
|
+
? [expected.pid, expected.startTime]
|
|
60
|
+
: run(["display-message", "-p", "#{pid}\t#{start_time}"]).split("\t");
|
|
61
|
+
if (!pid || !startTime) throw new Error("Invalid native tmux server identity");
|
|
62
|
+
const identity = { pid, startTime };
|
|
63
|
+
// Validate once before publishing an owner; each command rechecks natively.
|
|
64
|
+
fenceNativeTmuxCommand(["display-message", "-p", ""], identity);
|
|
65
|
+
return (args) => {
|
|
66
|
+
const command = fenceNativeTmuxCommand(args, identity);
|
|
67
|
+
return command.verify(run(command.argv));
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createServerGenerationFencedTmuxAsyncRunner(
|
|
72
|
+
authority: WorkspacePaneTmuxAuthority,
|
|
73
|
+
expected: NativeTmuxServerIdentity,
|
|
74
|
+
): (args: readonly string[], signal?: AbortSignal) => Promise<string> {
|
|
75
|
+
const run = createPinnedWorkspaceTmuxAsyncRunner(authority);
|
|
76
|
+
fenceNativeTmuxCommand(["display-message", "-p", ""], expected);
|
|
77
|
+
return async (args, signal) => {
|
|
78
|
+
const command = fenceNativeTmuxCommand(args, expected);
|
|
79
|
+
return command.verify(await run(command.argv, signal));
|
|
80
|
+
};
|
|
81
|
+
}
|