513 lines
24 KiB
Swift
513 lines
24 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
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 }
|
|
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
|
|
""" : "")
|
|
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
|
|
""" : "")
|
|
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 }
|
|
}
|
|
}
|