166 lines
8.1 KiB
Swift
166 lines
8.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
nonisolated enum RemoteZmxCommands {
|
|
static let root = #"""
|
|
if [ -n "${ZMX_DIR:-}" ]; then root=$ZMX_DIR
|
|
elif [ -n "${XDG_RUNTIME_DIR:-}" ]; then root="$XDG_RUNTIME_DIR/zmx"
|
|
else root="${TMPDIR:-/tmp}/zmx-$(id -u)"; fi
|
|
"""#
|
|
|
|
static var list: String {
|
|
"""
|
|
\(RemoteMultiplexerCommands.discovery(.zmx))
|
|
\(RemoteMultiplexerCommands.utilities)
|
|
\(root)
|
|
printf 'ROOT|%s\\n' "$(hex "$root")"
|
|
"$exe" list || exit 78
|
|
printf 'END\\n'
|
|
"""
|
|
}
|
|
|
|
static func parseList(_ text: String) throws -> [RemoteMultiplexerCandidate] {
|
|
var lines = text.split(separator: "\n").map(String.init)
|
|
guard text.hasSuffix("\n"), lines.count >= 2, lines[0].hasPrefix("ROOT|"), lines.last == "END" else {
|
|
throw RemoteSessionFailure.invalidResponse
|
|
}
|
|
lines.removeLast()
|
|
let root = try RemoteMultiplexerCommands.unhex(String(lines.removeFirst().dropFirst(5)))
|
|
guard RemoteMultiplexerCommands.validPath(root) else { throw RemoteSessionFailure.invalidResponse }
|
|
return try lines.map { line in
|
|
var fields: [String: String] = [:]
|
|
for (index, column) in line.split(separator: "\t").enumerated() {
|
|
// Recent releases indent the name column even outside a zmx session.
|
|
let part = index == 0 ? column.drop(while: { $0 == " " }) : column
|
|
guard let separator = part.firstIndex(of: "=") else { throw RemoteSessionFailure.invalidResponse }
|
|
let key = String(part[..<separator])
|
|
guard fields[key] == nil else { throw RemoteSessionFailure.invalidResponse }
|
|
fields[key] = String(part[part.index(after: separator)...])
|
|
}
|
|
guard let name = fields["name"], RemoteMultiplexerCommands.validName(name),
|
|
let pid = fields["pid"], UInt64(pid).map({ $0 > 1 }) == true,
|
|
let created = fields["created"], UInt64(created) != nil,
|
|
let clients = fields["clients"].flatMap(Int.init), clients >= 0, fields["err"] == nil
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
return .init(
|
|
name: name, root: root, socket: root + "/" + name, process: pid, created: created, clients: clients)
|
|
}
|
|
}
|
|
|
|
static func create(name: String, token: UUID) -> String {
|
|
"""
|
|
\(root)
|
|
export ZMX_DIR=$root
|
|
socket="$root/\(name)"
|
|
[ ! -e "$socket" ] && [ ! -L "$socket" ] || exit 79
|
|
native_started=1
|
|
"$exe" attach '\(name)' "$helper/shell" </dev/null >/dev/null 2>"$meta/create-error" || exit 78
|
|
"$exe" set '\(name)' 'termind_owner=\(token.uuidString)' >/dev/null || exit 78
|
|
native=$(\(nativeIdentity(name: name))) || exit 79
|
|
[ -n "$native" ] || exit 79
|
|
printf '%s\\n' "$native" > "$meta/native"
|
|
"""
|
|
}
|
|
|
|
// The listed PID is the PTY child. Labels or commands containing "pid=" are not identity fields.
|
|
static func nativeIdentity(name: String) -> String {
|
|
"""
|
|
"$exe" list | awk -F '\\t' -v target='\(name)' '
|
|
{ sub(/^ +/, "", $1); name=""; pid=""; created=""; count=0
|
|
for(i=1;i<=NF;i++) {
|
|
if ($i ~ /^name=/) {name=substr($i,6); count++}
|
|
if ($i ~ /^pid=[0-9]+$/) {pid=substr($i,5); count++}
|
|
if ($i ~ /^created=[0-9]+$/) {created=substr($i,9); count++}
|
|
}
|
|
if(name==target && count==3 && pid!="" && created!="") print pid ":" created
|
|
}'
|
|
"""
|
|
}
|
|
}
|
|
|
|
nonisolated struct RemoteZmxDialect: RemoteSocketMultiplexerDialect {
|
|
let id = SessionPersistenceBackendID.zmx
|
|
let paneVariable: String? = nil
|
|
let versionArgument = "version"
|
|
let capabilities = RemoteSessionCapabilities(clientCount: true, shellIntegration: true)
|
|
var list: String { RemoteZmxCommands.list }
|
|
|
|
func validateVersion(_ output: String) throws {
|
|
let version = try RemoteMultiplexerVersion(output, backend: id)
|
|
guard version.major > 0 || version.minor >= 7 else { throw RemoteSessionFailure.unsupported }
|
|
}
|
|
func validPane(_ value: String) -> Bool { value == "shell" }
|
|
func parseList(_ text: String) throws -> [RemoteMultiplexerCandidate] { try RemoteZmxCommands.parseList(text) }
|
|
func create(name: String, token: UUID) -> String { RemoteZmxCommands.create(name: name, token: token) }
|
|
func attach(name: String) -> String {
|
|
// attach is an upsert. A daemon lost after validation must not become an unrelated shell.
|
|
"\"$exe\" attach \(name) /bin/sh -c 'exit 79'"
|
|
}
|
|
func stop(name: String) -> String { "\"$exe\" kill \(name) --force" }
|
|
func validRoute(_ identity: RemoteMultiplexerIdentity, name: String) -> Bool {
|
|
identity.socket == identity.root + "/" + name
|
|
}
|
|
func routing(_ identity: RemoteMultiplexerIdentity) throws -> String {
|
|
"export ZMX_DIR=\(try RemoteMultiplexerCommands.quote(identity.root))"
|
|
}
|
|
func validateIdentity(_ identity: RemoteMultiplexerIdentity) throws {
|
|
guard !identity.isolated, identity.process != nil, identity.created != nil,
|
|
identity.daemonProcess.flatMap(UInt64.init).map({ $0 > 1 }) == true,
|
|
identity.daemonStarted?.isEmpty == false,
|
|
identity.daemonStarted!.utf8.allSatisfy({ (32...126).contains($0) })
|
|
else { throw RemoteSessionFailure.invalidResponse }
|
|
}
|
|
func identityGuard(_ identity: RemoteMultiplexerIdentity, name: String) throws -> String {
|
|
"""
|
|
[ "$(\(RemoteZmxCommands.nativeIdentity(name: name)))" = '\(identity.process!):\(identity.created!)' ] || exit 79
|
|
[ "$(ps -p '\(identity.process!)' -o ppid= | tr -d ' ')" = '\(identity.daemonProcess!)' ] || exit 79
|
|
[ "$(process_start '\(identity.daemonProcess!)')" = \(try RemoteMultiplexerCommands.quote(identity.daemonStarted!)) ] || exit 79
|
|
"""
|
|
}
|
|
func ownershipGuard(_ identity: RemoteMultiplexerIdentity, name: String, token: UUID) throws -> String {
|
|
"""
|
|
[ "$("$exe" get \(try RemoteMultiplexerCommands.quote(name)) termind_owner)" = '\(token.uuidString)' ] || exit 79
|
|
[ "$(cat "$meta/native" 2>/dev/null)" = '\(identity.process!):\(identity.created!)' ] || exit 79
|
|
"""
|
|
}
|
|
func metadataOwnershipGuard(_ candidate: RemoteMultiplexerCandidate) throws -> String {
|
|
"""
|
|
export ZMX_DIR=\(try RemoteMultiplexerCommands.quote(candidate.root))
|
|
if [ "$("$exe" get '\(candidate.name)' termind_owner)" != "$owner" ] \
|
|
|| [ "$(cat "$meta/native" 2>/dev/null)" != '\(candidate.process!):\(candidate.created!)' ]; then
|
|
owner=-; helper=-
|
|
fi
|
|
"""
|
|
}
|
|
func metadataFields(_ candidate: RemoteMultiplexerCandidate) throws -> String {
|
|
"""
|
|
# zmx's list PID is the PTY child, not the daemon. Bind its parent separately.
|
|
daemon=$(ps -p '\(candidate.process!)' -o ppid= | tr -d ' ') || exit 79
|
|
comm=$(ps -p "$daemon" -o comm=)
|
|
[ "${comm##*/}" = zmx ] || exit 79
|
|
born=$(process_start "$daemon")
|
|
[ -n "$born" ] || exit 79
|
|
born=$(hex "$born")
|
|
"""
|
|
}
|
|
func runtimeCommands(name: String) -> (String, String) { ("\"$exe\" list", "printf 'shell'") }
|
|
func activePane(
|
|
native: String, focus: String, reference: RemoteSessionReference, clientBound: Bool = false
|
|
) throws -> String? {
|
|
let identity = try RemoteMultiplexerIdentity.decode(reference.session.identity, kind: id)
|
|
let root = identity.root.utf8.map { String(format: "%02x", $0) }.joined()
|
|
let sessions = try RemoteZmxCommands.parseList("ROOT|\(root)\n" + native + "\nEND\n")
|
|
guard clientBound || sessions.count == 1,
|
|
let session = sessions.first(where: { $0.name == reference.session.name }),
|
|
session.name == reference.session.name, session.process == identity.process,
|
|
session.created == identity.created, session.clients == 1
|
|
else { return nil }
|
|
return "shell"
|
|
}
|
|
func writeDraft(name: String, pane: String, key: String) -> String { "\"$exe\" send \(name) \(key)" }
|
|
}
|