Extract multiplexer dialect and resumable terminal engine registries while preserving session and checkpoint compatibility. Share local-network and theme policies, add regression coverage, and update discovery actions to Cancel and Rescan with localized Telnet copy.
514 lines
25 KiB
Swift
514 lines
25 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
nonisolated struct RemoteMultiplexerIdentity: Codable, Equatable, Sendable {
|
|
var version = 1
|
|
// Keep the legacy raw-string discriminator, independently of the public backend ID's Codable shape.
|
|
private let kindValue: String
|
|
var kind: SessionPersistenceBackendID { .init(rawValue: kindValue) }
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case version, root, socket, fingerprint, process, created, owner, helper, isolated
|
|
case directoryFingerprint, daemonProcess, daemonStarted
|
|
case kindValue = "kind"
|
|
}
|
|
let root: String
|
|
let socket: String
|
|
let fingerprint: String
|
|
let process: String?
|
|
let created: String?
|
|
let owner: UUID?
|
|
let helper: String?
|
|
let isolated: Bool
|
|
var directoryFingerprint: String? = nil
|
|
var daemonProcess: String? = nil
|
|
var daemonStarted: String? = nil
|
|
|
|
init(
|
|
kind: SessionPersistenceBackendID, root: String, socket: String, fingerprint: String,
|
|
process: String?, created: String?, owner: UUID?, helper: String?, isolated: Bool,
|
|
directoryFingerprint: String? = nil, daemonProcess: String? = nil, daemonStarted: String? = nil
|
|
) {
|
|
kindValue = kind.rawValue
|
|
self.root = root
|
|
self.socket = socket
|
|
self.fingerprint = fingerprint
|
|
self.process = process
|
|
self.created = created
|
|
self.owner = owner
|
|
self.helper = helper
|
|
self.isolated = isolated
|
|
self.directoryFingerprint = directoryFingerprint
|
|
self.daemonProcess = daemonProcess
|
|
self.daemonStarted = daemonStarted
|
|
}
|
|
|
|
static func decode(_ data: Data, kind: SessionPersistenceBackendID) throws -> Self {
|
|
let value: Self
|
|
do { value = try JSONDecoder().decode(Self.self, from: data) } catch {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
guard RemoteMultiplexerRegistry.dialect(value.kind) != nil else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
guard value.version == 1, value.kind == kind,
|
|
let dialect = RemoteMultiplexerRegistry.socketDialect(kind)
|
|
else {
|
|
throw RemoteSessionFailure.unsupported
|
|
}
|
|
guard RemoteMultiplexerCommands.validPath(value.root), RemoteMultiplexerCommands.validPath(value.socket),
|
|
value.socket.hasPrefix(value.root + "/"),
|
|
value.fingerprint.split(separator: ":").count == 3,
|
|
value.fingerprint.utf8.allSatisfy({ (48...57).contains($0) || $0 == 58 }),
|
|
value.helper.map(RemoteMultiplexerCommands.validHelper) ?? true,
|
|
value.process.map({ UInt64($0).map { $0 > 1 } ?? false }) ?? true,
|
|
value.created.map({ UInt64($0) != nil }) ?? true,
|
|
value.directoryFingerprint.map({
|
|
$0.split(separator: ":").count == 2
|
|
&& $0.utf8.allSatisfy { (48...57).contains($0) || $0 == 58 }
|
|
}) ?? true
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
try dialect.validateIdentity(value)
|
|
return value
|
|
}
|
|
}
|
|
|
|
nonisolated struct RemoteMultiplexerCandidate {
|
|
let name: String
|
|
let root: String
|
|
let socket: String?
|
|
var process: String? = nil
|
|
var created: String? = nil
|
|
var clients: Int? = nil
|
|
var isolated = false
|
|
}
|
|
|
|
nonisolated enum RemoteMultiplexerCommands {
|
|
static func discovery(_ backend: SessionPersistenceBackendID) -> String {
|
|
"""
|
|
export LC_ALL=C
|
|
exe=$(command -v \(backend.rawValue) 2>/dev/null)
|
|
case "$exe" in /*) ;; *) exe= ;; esac
|
|
if [ -z "$exe" ]; then
|
|
for dir in /opt/homebrew/bin /usr/local/bin /usr/bin /snap/bin "$HOME/.local/bin" "$HOME/.cargo/bin"; do
|
|
if [ -f "$dir/\(backend.rawValue)" ] && [ -x "$dir/\(backend.rawValue)" ]; then exe="$dir/\(backend.rawValue)"; break; fi
|
|
done
|
|
fi
|
|
[ -n "$exe" ] && [ -x "$exe" ] || exit 77
|
|
unset ZELLIJ ZELLIJ_PANE_ID ZELLIJ_SESSION_NAME ZMX_SESSION ZMX_SESSION_PREFIX
|
|
unset HERDR_SESSION HERDR_SOCKET_PATH HERDR_CLIENT_SOCKET_PATH HERDR_PANE_ID HERDR_PANE_RUNTIME_ID
|
|
unset HERDR_ACTIVE_PANE_ID HERDR_ACTIVE_TAB_ID HERDR_ACTIVE_WORKSPACE_ID HERDR_ACTIVE_PANE_CWD
|
|
unset HERDR_WORKSPACE_ID HERDR_TAB_ID HERDR_STARTUP_CWD HERDR_BIN_PATH
|
|
"""
|
|
}
|
|
|
|
static func quote(_ value: String) throws -> String {
|
|
guard !value.contains("\0") else { throw RemoteSessionFailure.invalidResponse }
|
|
return "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
|
}
|
|
|
|
static func validPath(_ value: String) -> Bool {
|
|
value.hasPrefix("/") && value.utf8.count <= 4096
|
|
&& !value.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains)
|
|
&& !value.split(separator: "/").contains("..")
|
|
}
|
|
|
|
static func validHelper(_ value: String) -> Bool {
|
|
value.hasPrefix("/tmp/termind-shell.") && value.count > "/tmp/termind-shell.".count
|
|
&& value.dropFirst("/tmp/termind-shell.".count).utf8.allSatisfy {
|
|
(48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0)
|
|
}
|
|
}
|
|
|
|
static func validName(_ value: String) -> Bool {
|
|
!value.isEmpty && value.utf8.count <= 128 && value.first != "-"
|
|
&& value.utf8.allSatisfy {
|
|
(48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0)
|
|
|| [45, 46, 95].contains($0)
|
|
} && value != "." && value != ".."
|
|
}
|
|
|
|
static func managedName(_ token: UUID) -> String {
|
|
"termind-"
|
|
+ SHA256.hash(data: Data(token.uuidString.utf8)).prefix(8).map {
|
|
String(format: "%02x", $0)
|
|
}.joined()
|
|
}
|
|
|
|
static func unhex(_ value: String, limit: Int = 8192) throws -> String {
|
|
guard value.count.isMultiple(of: 2), value.count <= limit else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
var bytes: [UInt8] = []
|
|
var cursor = value.startIndex
|
|
while cursor != value.endIndex {
|
|
let end = value.index(cursor, offsetBy: 2)
|
|
guard let byte = UInt8(value[cursor..<end], radix: 16) else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
bytes.append(byte)
|
|
cursor = end
|
|
}
|
|
guard let result = String(bytes: bytes, encoding: .utf8) else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
return result
|
|
}
|
|
|
|
static let utilities = #"""
|
|
hex() { printf '%s' "$1" | od -An -v -tx1 | tr -d ' \n'; }
|
|
process_start() { LC_ALL=C TZ=UTC ps -p "$1" -o lstart= | sed 's/^ *//;s/ *$//'; }
|
|
fingerprint() {
|
|
[ -S "$1" ] && [ ! -L "$1" ] || return 1
|
|
stat -c '%d:%i:%Z' "$1" 2>/dev/null || stat -f '%d:%i:%c' "$1"
|
|
}
|
|
private_dir() {
|
|
[ -d "$1" ] || (umask 077; mkdir "$1") || return 1
|
|
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
|
owner=$(stat -c '%u' "$1" 2>/dev/null || stat -f '%u' "$1") || return 1
|
|
[ "$owner" = "$(id -u)" ] && chmod 700 "$1"
|
|
}
|
|
directory_fingerprint() {
|
|
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
|
stat -c '%d:%i' "$1" 2>/dev/null || stat -f '%d:%i' "$1"
|
|
}
|
|
private_state() {
|
|
for path in "/tmp/termind-sessions-$(id -u)" "$state" "$meta"; do
|
|
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
|
[ "$(stat -c '%u' "$path" 2>/dev/null || stat -f '%u' "$path")" = "$(id -u)" ] || return 1
|
|
done
|
|
for field in owner helper identity; do
|
|
[ -f "$meta/$field" ] && [ ! -L "$meta/$field" ] || return 1
|
|
done
|
|
}
|
|
"""#
|
|
|
|
static func stateRoot(_ kind: SessionPersistenceBackendID, create: Bool) -> String {
|
|
"""
|
|
state="/tmp/termind-sessions-$(id -u)"
|
|
\(create ? "private_dir \"$state\" || exit 78" : "")
|
|
state="$state/\(kind.rawValue)"
|
|
\(create ? "private_dir \"$state\" || exit 78" : "")
|
|
"""
|
|
}
|
|
|
|
static func environment(_ values: [HostEnvironmentVariable]) throws -> String {
|
|
try values.map { value in
|
|
guard value.isValid, !value.variable.hasPrefix("TERMIND_"),
|
|
!["TMUX", "ZELLIJ", "ZMX", "HERDR"].contains(where: {
|
|
value.variable == $0 || value.variable.hasPrefix($0 + "_")
|
|
})
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
return "export \(value.variable)=\(try quote(value.value))"
|
|
}.joined(separator: "\n")
|
|
}
|
|
|
|
static func routing(_ identity: RemoteMultiplexerIdentity) throws -> String {
|
|
guard let dialect = RemoteMultiplexerRegistry.socketDialect(identity.kind) else {
|
|
throw RemoteSessionFailure.unsupported
|
|
}
|
|
return try dialect.routing(identity)
|
|
}
|
|
|
|
static func guardIdentity(_ reference: RemoteSessionReference) throws -> String {
|
|
let kind = reference.backend
|
|
guard let dialect = RemoteMultiplexerRegistry.socketDialect(kind),
|
|
reference.version == 1, validName(reference.session.name)
|
|
else { throw RemoteSessionFailure.unsupported }
|
|
let identity = try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: kind)
|
|
try validateRoute(identity, name: reference.session.name)
|
|
if let owner = reference.ownershipToken, owner != identity.owner {
|
|
throw RemoteSessionFailure.ownershipMismatch
|
|
}
|
|
let owned =
|
|
try reference.ownershipToken.map { token in
|
|
let labelGuard = try dialect.ownershipGuard(identity, name: reference.session.name, token: token)
|
|
return """
|
|
\(stateRoot(kind, create: false))
|
|
meta="$state/\(reference.session.name)"
|
|
private_state || exit 79
|
|
[ "$(cat "$meta/owner" 2>/dev/null)" = '\(token.uuidString)' ] || exit 79
|
|
[ "$(cat "$meta/identity" 2>/dev/null)" = \(try quote(identity.fingerprint)) ] || exit 79
|
|
\(labelGuard)
|
|
"""
|
|
} ?? ""
|
|
return """
|
|
\(utilities)
|
|
\(discovery(kind))
|
|
\(try routing(identity))
|
|
[ "$(fingerprint \(try quote(identity.socket)))" = \(try quote(identity.fingerprint)) ] || exit 79
|
|
\(try dialect.identityGuard(identity, name: reference.session.name))
|
|
\(owned)
|
|
"""
|
|
}
|
|
|
|
static func validateRoute(_ identity: RemoteMultiplexerIdentity, name: String) throws {
|
|
guard let dialect = RemoteMultiplexerRegistry.socketDialect(identity.kind),
|
|
dialect.validRoute(identity, name: name)
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
_ = try routing(identity)
|
|
}
|
|
}
|
|
|
|
/// Shared ownership and framing policy; native command dialects remain backend-owned.
|
|
nonisolated struct RemoteMultiplexerBackend: RemoteSessionBackend {
|
|
let dialect: any RemoteSocketMultiplexerDialect
|
|
var kind: SessionPersistenceBackendID { dialect.id }
|
|
var id: SessionPersistenceBackendID { dialect.id }
|
|
let execute: RemoteSessionCommandExecutor
|
|
|
|
var capabilities: RemoteSessionCapabilities { dialect.capabilities }
|
|
|
|
func probe() async throws -> RemoteSessionCapabilities { try await dialect.probe(execute: execute) }
|
|
|
|
static func check(_ result: RemoteSessionCommandResult) throws {
|
|
guard !result.truncated, result.stdout.utf8.count <= 1024 * 1024 else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
switch result.exitCode {
|
|
case 0: return
|
|
case 77: throw RemoteSessionFailure.missing
|
|
case 79: throw RemoteSessionFailure.ownershipMismatch
|
|
case 80: throw RemoteSessionFailure.unsupported
|
|
case 81: throw RemoteSessionFailure.startupUnavailable
|
|
default: throw RemoteSessionFailure.commandFailed(result.stderr)
|
|
}
|
|
}
|
|
|
|
func validate(_ reference: RemoteSessionReference) throws {
|
|
guard reference.version == 1, reference.backend == kind,
|
|
RemoteMultiplexerCommands.validName(reference.session.name)
|
|
else { throw RemoteSessionFailure.unsupported }
|
|
let identity = try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: kind)
|
|
try RemoteMultiplexerCommands.validateRoute(identity, name: reference.session.name)
|
|
guard reference.session.id == (identity.isolated ? "managed:" : "user:") + reference.session.name,
|
|
reference.ownershipToken == nil || reference.ownershipToken == identity.owner,
|
|
reference.ownershipToken.map({ RemoteMultiplexerCommands.managedName($0) == reference.session.name })
|
|
?? true
|
|
else { throw RemoteSessionFailure.ownershipMismatch }
|
|
}
|
|
|
|
func matches(_ reference: RemoteSessionReference, session: RemoteSessionDescriptor) -> Bool {
|
|
guard (try? validate(reference)) != nil, reference.session.id == session.id,
|
|
let saved = try? RemoteMultiplexerIdentity.decode(reference.session.identity, kind: kind),
|
|
let current = try? RemoteMultiplexerIdentity.decode(session.identity, kind: kind)
|
|
else { return false }
|
|
return saved.root == current.root && saved.socket == current.socket
|
|
&& saved.fingerprint == current.fingerprint && saved.process == current.process
|
|
&& saved.created == current.created
|
|
&& saved.directoryFingerprint == current.directoryFingerprint
|
|
&& saved.daemonProcess == current.daemonProcess && saved.daemonStarted == current.daemonStarted
|
|
&& (reference.ownershipToken == nil || reference.ownershipToken == current.owner)
|
|
}
|
|
|
|
func sessions() async throws -> [RemoteSessionDescriptor] {
|
|
let response = try await execute(dialect.list)
|
|
try Self.check(response)
|
|
let candidates = try dialect.parseList(response.stdout)
|
|
guard candidates.count <= 256, Set(candidates.map { "\($0.isolated):\($0.name)" }).count == candidates.count
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
guard !candidates.isEmpty else { return [] }
|
|
let commands = try candidates.enumerated().map { index, candidate -> String in
|
|
guard RemoteMultiplexerCommands.validName(candidate.name),
|
|
RemoteMultiplexerCommands.validPath(candidate.root)
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
let socket: String
|
|
if let path = candidate.socket {
|
|
guard RemoteMultiplexerCommands.validPath(path), path.hasPrefix(candidate.root + "/") else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
socket = "socket=\(try RemoteMultiplexerCommands.quote(path))"
|
|
} else {
|
|
socket = """
|
|
socket=$(find \(try RemoteMultiplexerCommands.quote(candidate.root)) -type s -name \
|
|
\(try RemoteMultiplexerCommands.quote(candidate.name)) -print 2>/dev/null) || exit 78
|
|
"""
|
|
}
|
|
return """
|
|
\(socket)
|
|
fp=$(fingerprint "$socket") || exit 79
|
|
owner=-; helper=-; directory=-; daemon=-; born=-
|
|
meta="$state/\(candidate.name)"
|
|
if private_state && [ "$(cat "$meta/identity" 2>/dev/null)" = "$fp" ]; then
|
|
owner=$(cat "$meta/owner") || exit 78
|
|
helper=$(cat "$meta/helper") || exit 78
|
|
\(try dialect.metadataOwnershipGuard(candidate))
|
|
fi
|
|
\(try dialect.metadataFields(candidate))
|
|
printf 'F|\(index)|%s|%s|%s|%s|%s|%s|%s\\n' "$(hex "$socket")" "$fp" "$owner" "$helper" "$directory" "$daemon" "$born"
|
|
"""
|
|
}.joined(separator: "\n")
|
|
let metadata = try await execute(
|
|
RemoteMultiplexerCommands.discovery(kind) + "\n" + RemoteMultiplexerCommands.utilities + "\n"
|
|
+ RemoteMultiplexerCommands.stateRoot(kind, create: false)
|
|
+ "\n" + commands)
|
|
try Self.check(metadata)
|
|
let lines = metadata.stdout.split(separator: "\n")
|
|
guard metadata.stdout.hasSuffix("\n"), lines.count == candidates.count else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
return try lines.enumerated().map { index, line in
|
|
let fields = line.split(separator: "|", omittingEmptySubsequences: false).map(String.init)
|
|
guard fields.count == 9, fields[0] == "F", fields[1] == String(index),
|
|
fields[4] == "-" || UUID(uuidString: fields[4]) != nil,
|
|
fields[5] == "-" || RemoteMultiplexerCommands.validHelper(fields[5])
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
let candidate = candidates[index]
|
|
let identity = RemoteMultiplexerIdentity(
|
|
kind: kind, root: candidate.root, socket: try RemoteMultiplexerCommands.unhex(fields[2]),
|
|
fingerprint: fields[3], process: candidate.process, created: candidate.created,
|
|
owner: UUID(uuidString: fields[4]), helper: fields[5] == "-" ? nil : fields[5],
|
|
isolated: candidate.isolated, directoryFingerprint: fields[6] == "-" ? nil : fields[6],
|
|
daemonProcess: fields[7] == "-" ? nil : fields[7],
|
|
daemonStarted: fields[8] == "-" ? nil : try RemoteMultiplexerCommands.unhex(fields[8]))
|
|
let data = try JSONEncoder().encode(identity)
|
|
_ = try RemoteMultiplexerIdentity.decode(data, kind: kind)
|
|
try RemoteMultiplexerCommands.validateRoute(identity, name: candidate.name)
|
|
return RemoteSessionDescriptor(
|
|
id: (candidate.isolated ? "managed:" : "user:") + candidate.name,
|
|
name: candidate.name, identity: data, clients: candidate.clients)
|
|
}
|
|
}
|
|
|
|
func create(
|
|
token: UUID, startupCommand: String?, shellIntegration: Bool, environment: [HostEnvironmentVariable]
|
|
) async throws -> RemoteSessionDescriptor {
|
|
let name = RemoteMultiplexerCommands.managedName(token)
|
|
if let existing = try await sessions().first(where: { $0.name == name }) {
|
|
let identity = try RemoteMultiplexerIdentity.decode(existing.identity, kind: kind)
|
|
guard identity.owner == token else { throw RemoteSessionFailure.ownershipMismatch }
|
|
// An interrupted response may leave a confirmed incarnation without its ready marker.
|
|
// Only finish that same incarnation; never replay a startup command or adopt by name.
|
|
let reference = RemoteSessionReference(
|
|
backend: kind, session: existing, hostID: UUID(), endpoint: "", username: "", ownershipToken: token)
|
|
let completion = try await execute(
|
|
"""
|
|
\(try RemoteMultiplexerCommands.guardIdentity(reference))
|
|
helper=$(cat "$meta/helper") || exit 79
|
|
\(startupCommand?.isEmpty == false ? """
|
|
[ -f "$helper/started" ] && [ ! -L "$helper/started" ] \
|
|
&& [ "$(cat "$helper/started")" = '\(token.uuidString)' ] || exit 81
|
|
""" : "")
|
|
: > "$meta/ready"
|
|
printf 'TERMIND_CREATED\\n'
|
|
""")
|
|
try Self.check(completion)
|
|
guard completion.stdout == "TERMIND_CREATED\n" else { throw RemoteSessionFailure.invalidResponse }
|
|
return existing
|
|
}
|
|
let native = dialect.create(name: name, token: token)
|
|
let requireStartup = startupCommand?.isEmpty == false
|
|
let shell =
|
|
try environment.last(where: { $0.variable == "SHELL" }).map {
|
|
try RemoteMultiplexerCommands.quote($0.value)
|
|
} ?? "\"${SHELL:-/bin/sh}\""
|
|
let result = try await execute(
|
|
"""
|
|
export TERMIND_SHELL_LOCALE_SET="${LC_ALL+x}" TERMIND_SHELL_LOCALE="${LC_ALL-}"
|
|
\(RemoteMultiplexerCommands.discovery(kind))
|
|
\(RemoteMultiplexerCommands.utilities)
|
|
\(RemoteMultiplexerCommands.stateRoot(kind, create: true))
|
|
umask 077
|
|
lock="$state/\(name).lock"
|
|
mkdir "$lock" 2>/dev/null || exit 79
|
|
reserved=0; native_started=0
|
|
cleanup() {
|
|
code=$?
|
|
if [ "$reserved" = 1 ] && [ "$native_started" = 0 ]; then
|
|
rm -f "$meta/owner" "$meta/helper" "$meta/create-error"
|
|
rmdir "$meta" 2>/dev/null
|
|
fi
|
|
rmdir "$lock" 2>/dev/null
|
|
exit "$code"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 78' HUP INT TERM
|
|
session_shell=\(shell)
|
|
\(requireStartup ? "case \"$session_shell\" in */bash|*/zsh|*/fish) [ -x \"$session_shell\" ] || exit 81 ;; *) exit 81 ;; esac" : "")
|
|
\(try RemoteShellStartup.install(
|
|
token: token, startupCommand: startupCommand, shellIntegration: shellIntegration, backend: kind.rawValue,
|
|
environment: environment))
|
|
meta="$state/\(name)"
|
|
# A reservation with no confirmed incarnation is not permission to adopt a same-named session.
|
|
mkdir "$meta" 2>/dev/null || exit 79
|
|
reserved=1
|
|
chmod 700 "$meta" || exit 78
|
|
printf '%s\\n' '\(token.uuidString)' > "$meta/owner" || exit 78
|
|
printf '%s\\n' "$helper" > "$meta/helper" || exit 78
|
|
export TERMIND_TMUX_HELPERS="$helper" TERMIND_TMUX_SHELL="$session_shell"
|
|
\(native)
|
|
fp=$(fingerprint "$socket") || exit 79
|
|
printf '%s\\n' "$fp" > "$meta/identity" || exit 78
|
|
\(requireStartup ? """
|
|
attempt=0
|
|
while [ ! -f "$helper/started" ]; do
|
|
attempt=$((attempt + 1))
|
|
if [ "$attempt" -ge 50 ]; then
|
|
# A slow or bypassed user rc must not run the snippet after reporting failure.
|
|
mkdir "$helper/startup-decision" 2>/dev/null && exit 81
|
|
[ -f "$helper/started" ] || exit 81
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
[ "$(cat "$helper/started")" = '\(token.uuidString)' ] || exit 81
|
|
""" : "")
|
|
: > "$meta/ready"
|
|
printf 'TERMIND_CREATED\\n'
|
|
""")
|
|
try Self.check(result)
|
|
guard result.stdout == "TERMIND_CREATED\n",
|
|
let session = try await sessions().first(where: { $0.name == name }),
|
|
try RemoteMultiplexerIdentity.decode(session.identity, kind: kind).owner == token
|
|
else { throw RemoteSessionFailure.ownershipMismatch }
|
|
return session
|
|
}
|
|
|
|
func launch(_ reference: RemoteSessionReference, created: Bool) throws -> RemoteSessionLaunch {
|
|
try validate(reference)
|
|
let identity = try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: kind)
|
|
let name = try RemoteMultiplexerCommands.quote(reference.session.name)
|
|
let attach = dialect.attach(name: name)
|
|
let noResurrection = dialect.prepareAttachment()
|
|
let bind: String
|
|
if let helper = identity.helper {
|
|
bind = """
|
|
helper=\(try RemoteMultiplexerCommands.quote(helper))
|
|
tty=$(tty) || exit 79
|
|
key=$(printf '%s' "$tty" | od -An -v -tx1 | tr -d ' \\n')
|
|
printf '%s|%s\\n' "$$" "$(process_start "$$")" > "$helper/client-$key"
|
|
"""
|
|
} else {
|
|
bind = ""
|
|
}
|
|
return .init(
|
|
command: try RemoteMultiplexerCommands.guardIdentity(reference) + "\n" + noResurrection
|
|
+ "\n" + bind + "\nexec " + attach,
|
|
attachment: reference, created: created)
|
|
}
|
|
|
|
func terminate(_ reference: RemoteSessionReference) async throws {
|
|
try validate(reference)
|
|
guard reference.isManaged else { throw RemoteSessionFailure.ownershipMismatch }
|
|
let identity = try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: kind)
|
|
try dialect.validateTermination(reference, identity: identity)
|
|
let name = try RemoteMultiplexerCommands.quote(reference.session.name)
|
|
let stop = dialect.stop(name: name)
|
|
let result = try await execute(
|
|
"""
|
|
\(try RemoteMultiplexerCommands.guardIdentity(reference))
|
|
\(stop) >/dev/null || exit 78
|
|
attempt=0
|
|
while [ -S \(try RemoteMultiplexerCommands.quote(identity.socket)) ]; do
|
|
attempt=$((attempt + 1)); [ "$attempt" -lt 50 ] || exit 79
|
|
sleep 0.1
|
|
done
|
|
\(try dialect.terminationCleanup(reference, identity: identity))
|
|
printf 'TERMIND_ENDED\\n'
|
|
""")
|
|
try Self.check(result)
|
|
guard result.stdout == "TERMIND_ENDED\n" else { throw RemoteSessionFailure.invalidResponse }
|
|
}
|
|
}
|