Files
Termind/Frameworks/TermindRemoteInfrastructure/SSH/RemoteTerminalDraft.swift
T
Kaede Akino e457e9af98 fix(multiplexer): preserve session ownership and safe shell startup
Detach managed sessions when native mobile windows are discarded, retain
live reopen references, and fence ordinary startup commands by input and
session generation. Handle noexec helpers, isolate tmux discovery locale,
and count only verified live contexts against the journal limit.

Move actual shell execution coverage into the hostless framework suite.
Validation: 76 focused macOS tests passed; iOS multiplexer regressions
passed with the inherited hosted-test artwork failure still present.
2026-10-17 09:50:37 +08:00

90 lines
4.6 KiB
Swift

import Foundation
import TermindApplication
import TermindDomain
nonisolated enum RemoteTerminalDraft {
static func command(
context: TerminalShellContext, terminal: String, helper: String?,
clientIdentity: String?, request: UUID, replacement: String?, original: String? = nil,
charset: RemoteCharset = .utf8, target: RemoteMultiplexerDraftTarget? = nil
) throws -> String {
let identity = context.identity
guard context.promptReady, ["bash", "zsh", "fish"].contains(context.shell),
identity.shellInstance.hasPrefix("ctx."),
identity.shellInstance.utf8.allSatisfy({
(48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0) || $0 == 46
}), replacement?.utf8.count ?? 0 <= 4096, replacement?.contains("\0") != true,
original?.utf8.count ?? 0 <= 4096, original?.contains("\0") != true
else { throw TerminalRuntimeError.staleContext }
let root: String
let trigger: String
if let target {
guard RemoteMultiplexerRegistry.dialect(.init(rawValue: identity.backend)) != nil else {
throw TerminalRuntimeError.staleContext
}
root = target.root
trigger = target.trigger
} else if identity.backend == "tmux" {
let target = try RemoteTmuxRuntimeDialect.draftTarget(
context: context, terminal: terminal, clientIdentity: clientIdentity, replacing: replacement != nil)
root = target.root
trigger = target.trigger
} else {
guard identity.backend == "shell", let helper else { throw TerminalRuntimeError.staleContext }
root = "helper=\(try RemoteMultiplexerCommands.quote(helper))"
// The worker sends the negotiated widget key after this request is installed.
trigger = "printf 'READY\\n'"
}
let bytes = try RemoteCharsetStreamConverter.encode(replacement ?? "", charset: charset)
guard bytes.count <= 4096 else { throw TerminalRuntimeError.staleContext }
let encoded = bytes.map { String(format: "%%%02X", $0) }.joined()
let baselineBytes = try original.map { try RemoteCharsetStreamConverter.encode($0, charset: charset) }
guard baselineBytes?.count ?? 0 <= 4096 else { throw TerminalRuntimeError.staleContext }
let baseline = baselineBytes?.map { String(format: "%%%02X", $0) }.joined() ?? "-"
return """
umask 077
\(root)
case "$helper" in /tmp/termind-shell.*) ;; *) exit 79 ;; esac
suffix=${helper#/tmp/termind-shell.}
case "$suffix" in ''|*[!a-zA-Z0-9]*) exit 79 ;; esac
dir="$helper/\(identity.shellInstance)"
[ -d "$dir" ] && [ ! -L "$dir" ] || exit 79
IFS='|' read -r tag seq ready cwd < "$dir/state" || exit 79
[ "$seq" = '\(context.sequence)' ] && [ "$ready" = 1 ] || exit 79
printf '%s|%s|%s|%s|%s|%s\\n' '\(request.uuidString)' '\(context.sequence)' \
'\(replacement == nil ? "read" : "replace")' '\(encoded)' "$(( $(date +%s) + 2 ))" '\(baseline)' > "$dir/request.tmp"
mv "$dir/request.tmp" "$dir/request"
\(trigger)
"""
}
static func resultCommand(context: TerminalShellContext, helper: String?, request: UUID) throws -> String {
let root: String
if context.identity.backend == "tmux" {
guard let session = context.identity.session.split(separator: ":").first else {
throw TerminalRuntimeError.staleContext
}
root = """
\(RemoteTmuxCommands.executionPreamble)
helper=$(tmux show-environment -t \(try RemoteMultiplexerCommands.quote(String(session))) TERMIND_TMUX_HELPERS) || exit 79
helper=${helper#TERMIND_TMUX_HELPERS=}
"""
} else {
guard let helper else { throw TerminalRuntimeError.staleContext }
root = "helper=\(try RemoteMultiplexerCommands.quote(helper))"
}
return """
\(root)
case "$helper" in /tmp/termind-shell.*) ;; *) exit 79 ;; esac
suffix=${helper#/tmp/termind-shell.}
case "$suffix" in ''|*[!a-zA-Z0-9]*) exit 79 ;; esac
file="$helper/\(context.identity.shellInstance)/journal"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if [ -f "$file" ] && grep -F '|draft|' "$file" | grep -q -F '|\(request.uuidString)'; then cat "$file"; exit 0; fi
sleep 0.1
done
exit 79
"""
}
}