diff --git a/server.js b/server.js index 98bf2b2..b9bdb00 100644 --- a/server.js +++ b/server.js @@ -153,8 +153,16 @@ globalThis.__terminalHandleExecMessage = (msg) => { } if (msg.type === 'exec_output') { - const data = Buffer.from(msg.data, 'base64').toString('utf-8'); - session.ws.send(JSON.stringify({ type: 'output', data })); + const bytes = Buffer.from(msg.data, 'base64'); + // Attach sessions carry a stream state: demultiplex non-TTY frames before + // forwarding. Exec sessions are raw TTY text and pass straight through. + if (session.streamState) { + for (const text of processDockerStreamChunk(bytes, session.streamState)) { + if (text) session.ws.send(JSON.stringify({ type: 'output', data: text })); + } + } else { + session.ws.send(JSON.stringify({ type: 'output', data: bytes.toString('utf-8') })); + } return; } @@ -430,14 +438,21 @@ async function handleTerminalConnection(ws, url, connId) { target = { type: 'socket', connectionType: 'socket', socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock' }; } - // Hawser Edge currently exposes an exec-only terminal protocol. + // Hawser Edge relays exec and (for capable agents) attach through the agent. if (target.connectionType === 'hawser-edge') { + let multiplexed = false; if (mode === 'attach') { - ws.send(JSON.stringify({ type: 'error', message: 'Container attach is not supported for Edge environments' })); - ws.close(); - return; + let containerTty = false; + if (typeof globalThis.__terminalGetContainerTty === 'function') { + try { + containerTty = await globalThis.__terminalGetContainerTty(containerId, envId); + } catch { + // Keep multiplexing enabled if the TTY setting cannot be read. + } + } + multiplexed = !containerTty; } - handleEdgeExec(ws, connId, containerId, shell, user, target.environmentId); + handleEdgeExec(ws, connId, containerId, shell, user, target.environmentId, mode, multiplexed); return; } @@ -584,20 +599,27 @@ async function handleTerminalConnection(ws, url, connId) { } /** - * Handle Hawser Edge exec session. - * Sends exec commands through the Hawser WebSocket relay. + * Handle Hawser Edge exec or attach session. + * Sends exec/attach commands through the Hawser WebSocket relay. Attach reuses the + * exec_* protocol with attach:true; for non-TTY containers the agent pipes a + * multiplexed stream, demultiplexed here via the session's stream state. */ -function handleEdgeExec(ws, connId, containerId, shell, user, environmentId) { +function handleEdgeExec(ws, connId, containerId, shell, user, environmentId, mode = 'exec', multiplexed = false) { if (typeof globalThis.__hawserSendMessage !== 'function') { ws.send(JSON.stringify({ type: 'error', message: 'Edge agent handler not ready' })); ws.close(); return; } + const attach = mode === 'attach'; const execId = randomUUID(); - edgeExecSessions.set(execId, { ws, execId, environmentId }); + // Attach output is a raw hijacked stream (no HTTP headers); only the multiplexing + // demux is needed, so seed the state with headersStripped already true. + const streamState = attach ? createDockerStreamState(multiplexed) : null; + if (streamState) streamState.headersStripped = true; + edgeExecSessions.set(execId, { ws, execId, environmentId, streamState }); - // Send exec_start to the Hawser agent + // Send exec_start (attach:true reuses the exec relay) to the Hawser agent const execStartMsg = JSON.stringify({ type: 'exec_start', execId, @@ -605,7 +627,8 @@ function handleEdgeExec(ws, connId, containerId, shell, user, environmentId) { cmd: shell, user, cols: 120, - rows: 30 + rows: 30, + attach }); const sent = globalThis.__hawserSendMessage(environmentId, execStartMsg); diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index a0663b9..1910e32 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -134,9 +134,6 @@ // Derived: current environment details for reactive port URL generation const currentEnvDetails = $derived($environments.find(e => e.id === $currentEnvironment?.id) ?? null); - // hawser-edge exposes an exec-only terminal protocol (the agent has no attach path), - // so don't offer Attach there - the server would reject it anyway. - const attachSupported = $derived(currentEnvDetails?.connectionType !== 'hawser-edge'); // Search and sort state - initialize from URL for persistence across navigation const initialSearch = $page.url.searchParams.get('search') @@ -1133,8 +1130,7 @@ } function startTerminal(container: ContainerInfo) { - // Edge is exec-only; never start an attach session there even if a stale mode lingers. - const mode: TerminalMode = attachSupported ? terminalMode : 'exec'; + const mode: TerminalMode = terminalMode; if (mode === 'exec') saveUserForContainer(container.id, terminalUser); terminalCustomUsers = getCustomUsers(); const terminal: ActiveTerminal = { @@ -2205,8 +2201,6 @@ {:else}
- - {#if attachSupported}
{ @@ -2234,7 +2228,6 @@
- {/if} {#if terminalMode === 'exec'}
diff --git a/src/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte index 6c8b27f..32e9e46 100644 --- a/src/routes/terminal/+page.svelte +++ b/src/routes/terminal/+page.svelte @@ -33,11 +33,6 @@ let shellDetection = $state(null); let detectingShells = $state(false); - // hawser-edge exposes an exec-only terminal protocol, so attach isn't offered there. - const attachSupported = $derived( - ($environments.find(e => e.id === $currentEnvironment?.id)?.connectionType ?? 'socket') !== 'hawser-edge' - ); - // Shell/user options let terminalMode = $state('exec'); let selectedShell = $state('/bin/bash'); @@ -221,7 +216,7 @@ onMount(async () => { customUsers = getCustomUsers(); - if ($page.url.searchParams.get('mode') === 'attach' && attachSupported) { + if ($page.url.searchParams.get('mode') === 'attach') { terminalMode = 'attach'; } await fetchContainers(); @@ -316,8 +311,6 @@
{/if}
- - {#if attachSupported}
terminalMode = value as TerminalMode}> @@ -342,7 +335,6 @@
- {/if} {#if selectedContainer}