Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/App/TmuxAttachedClientGuard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ enum TmuxAttachedClientGuard {
socketName: String?,
hookIndex: Int
) -> String {
var arguments = [tmuxPath]
var arguments = TmuxSocketEnvironment.commandPrefix(socketName: socketName) + [tmuxPath]
if let socketName, !socketName.isEmpty {
arguments += ["-L", socketName]
}
Expand All @@ -106,7 +106,7 @@ enum TmuxAttachedClientGuard {
tmuxPath: String,
socketName: String?
) -> String {
var arguments = [tmuxPath]
var arguments = TmuxSocketEnvironment.commandPrefix(socketName: socketName) + [tmuxPath]
if let socketName, !socketName.isEmpty {
arguments += ["-L", socketName]
}
Expand Down
146 changes: 100 additions & 46 deletions Sources/App/TmuxBinaryResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import GhosthubWorkspace
enum TmuxBinaryError: Error, Equatable, LocalizedError, Sendable {
case notFound(shell: String)
case shellFailed(status: Int32)
case kwtDiscoveryFailed(sessions: [DiscoveredTmuxSession], status: Int32)
case sshConnectionFailed(
host: String,
classification: SSHConnectionFailure.Classification
Expand All @@ -26,6 +27,9 @@ enum TmuxBinaryError: Error, Equatable, LocalizedError, Sendable {
case let .shellFailed(status):
return "The login shell exited with status \(status) while"
+ " locating tmux. Check your shell startup files."
case let .kwtDiscoveryFailed(_, status):
return "The kwt tmux server could not be read (status \(status))."
+ " Default-server sessions are available; kwt sessions may be out of date."
case let .sshConnectionFailed(host, classification):
return "\(classification.diagnostic.summary) Host: \(host). "
+ classification.diagnostic.recoverySuggestion
Expand All @@ -52,6 +56,7 @@ enum TmuxBinaryError: Error, Equatable, LocalizedError, Sendable {

struct DiscoveredTmuxSession: Equatable, Sendable {
var name: String
var socketName: String?
var windowCount: Int
var serverPID: String?
var sessionID: String?
Expand All @@ -60,8 +65,20 @@ struct DiscoveredTmuxSession: Equatable, Sendable {
var previewClientSize: TmuxGridSize?
var managed: Bool

var summary: TmuxSessionSummary {
TmuxSessionSummary(
name: name, socketName: socketName, managed: managed,
windows: (0 ..< windowCount).map {
TmuxWindowSummary(id: "discovered-\($0)", index: $0, name: "")
},
serverPID: serverPID, sessionID: sessionID, createdAt: createdAt,
activeWindowSize: activeWindowSize, previewClientSize: previewClientSize
)
}

init(
name: String,
socketName: String? = nil,
windowCount: Int,
serverPID: String? = nil,
sessionID: String? = nil,
Expand All @@ -71,6 +88,7 @@ struct DiscoveredTmuxSession: Equatable, Sendable {
managed: Bool
) {
self.name = name
self.socketName = socketName
self.windowCount = windowCount
self.serverPID = serverPID
self.sessionID = sessionID
Expand Down Expand Up @@ -342,28 +360,44 @@ struct TmuxBinaryResolver: Sendable {
+ "\"$ghosthub_tmux_path\" -V || exit $?"

private static let discoveryPrefix = "GHOSTHUB_TMUX_SESSION"
private static let kwtFailureMarker = "GHOSTHUB_TMUX_KWT_UNAVAILABLE"
private static let sessionPresentMarker =
"GHOSTHUB_TMUX_SESSION_PRESENT"
private static let sessionAbsentMarker =
"GHOSTHUB_TMUX_SESSION_ABSENT"
private static let discoveryFormat = discoveryPrefix
+ "\t#{session_windows}\t#{pid}\t#{session_id}\t#{session_created}"
+ "\t#{window_width}\t#{window_height}"
+ "\t#{status}"
+ "\t#{@ghosthub_owner}\t#{session_name}"
private static let discoveryCommand = probeCommand
+ "; ghosthub_tmux_output=$("
+ "\"$ghosthub_tmux_path\" list-sessions -F "
+ shellQuotedCommandArgument(discoveryFormat)
+ " 2>&1); ghosthub_tmux_status=$?; "
+ "if [ \"$ghosthub_tmux_status\" -eq 0 ]; then "
+ "printf '%s\\n' \"$ghosthub_tmux_output\"; else "
+ "printf '%s\\n' \"$ghosthub_tmux_output\" >&2; "
+ "case \"$ghosthub_tmux_output\" in "
+ "*\"no server running on \"*|"
+ "*\"failed to connect to server: No such file or directory\"*|"
+ "*\"error connecting to \"*\" (No such file or directory)\"*) "
+ "exit 0 ;; *) exit \"$ghosthub_tmux_status\" ;; esac; fi"
private static func discoveryFormat(socketName: String?) -> String {
discoveryPrefix + "\t\(socketName ?? "")"
+ "\t#{session_windows}\t#{pid}\t#{session_id}\t#{session_created}"
+ "\t#{window_width}\t#{window_height}"
+ "\t#{status}"
+ "\t#{@ghosthub_owner}\t#{session_name}"
}

private static let discoveryCommand: String = {
let socketNames: [String?] = [nil, "kwt"]
let queries = socketNames.map { socketName -> String in
let prefix = TmuxSocketEnvironment.commandPrefix(socketName: socketName)
.map { shellQuotedCommandArgument($0) + " " }.joined()
let failure = socketName == "kwt"
? "printf '%s\\t%s\\n' \(kwtFailureMarker) \"$ghosthub_tmux_status\""
: "exit \"$ghosthub_tmux_status\""
return "; ghosthub_tmux_output=$(" + prefix
+ "\"$ghosthub_tmux_path\" -L "
+ shellQuotedCommandArgument(socketName ?? "default")
+ " list-sessions -F "
+ shellQuotedCommandArgument(discoveryFormat(socketName: socketName))
+ " 2>&1); ghosthub_tmux_status=$?; "
+ "if [ \"$ghosthub_tmux_status\" -eq 0 ]; then "
+ "printf '%s\\n' \"$ghosthub_tmux_output\"; else "
+ "printf '%s\\n' \"$ghosthub_tmux_output\" >&2; "
+ "case \"$ghosthub_tmux_output\" in "
+ "*\"no server running on \"*|"
+ "*\"failed to connect to server: No such file or directory\"*|"
+ "*\"error connecting to \"*\" (No such file or directory)\"*) "
+ ": ;; *) \(failure) ;; esac; fi"
}
return probeCommand + queries.joined()
}()

private static func probeCommand(
for platform: SSHHostInfo.Platform
Expand Down Expand Up @@ -395,21 +429,30 @@ struct TmuxBinaryResolver: Sendable {
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$ghosthubMuxOutput = (& $ghosthubMux 'list-sessions' '-F' \(
powerShellEncodedArgument(discoveryFormat)
) 2>&1 | Out-String)
$ghosthubMuxStatus = $LASTEXITCODE
if ($ghosthubMuxStatus -eq 0) {
Write-Output $ghosthubMuxOutput
exit 0
}
[Console]::Error.Write($ghosthubMuxOutput)
if ($ghosthubMuxOutput -match 'no server running on ' -or
$ghosthubMuxOutput -match 'failed to connect to server: No such file or directory') {
exit 0
}
exit $ghosthubMuxStatus
"""
""" + [nil, "kwt"].map { socketName -> String in
let failure = socketName == "kwt"
? "Write-Output \"\(kwtFailureMarker)`t$ghosthubMuxStatus\""
: "exit $ghosthubMuxStatus"
return """

\(TmuxSocketEnvironment.powerShellPrelude(socketName: socketName))
$ghosthubMuxOutput = (& $ghosthubMux '-L' \(
powerShellEncodedArgument(socketName ?? "default")
) 'list-sessions' '-F' \(
powerShellEncodedArgument(discoveryFormat(socketName: socketName))
) 2>&1 | Out-String)
$ghosthubMuxStatus = $LASTEXITCODE
if ($ghosthubMuxStatus -eq 0) {
Write-Output $ghosthubMuxOutput
} else {
[Console]::Error.Write($ghosthubMuxOutput)
if ($ghosthubMuxOutput -notmatch 'no server running on ' -and
$ghosthubMuxOutput -notmatch 'failed to connect to server: No such file or directory') {
\(failure)
}
}
"""
}.joined(separator: "\n") + "\nexit 0"
}
}

Expand All @@ -428,8 +471,10 @@ struct TmuxBinaryResolver: Sendable {
let command = arguments
.map(shellQuotedCommandArgument)
.joined(separator: " ")
let prefix = TmuxSocketEnvironment.commandPrefix(socketName: socketName)
.map { shellQuotedCommandArgument($0) + " " }.joined()
return probeCommand
+ "; ghosthub_probe_error=$("
+ "; ghosthub_probe_error=$(" + prefix
+ "\"$ghosthub_tmux_path\" \(command) 2>&1); "
+ "ghosthub_probe_status=$?; "
+ "if [ \"$ghosthub_probe_status\" -eq 0 ]; then "
Expand Down Expand Up @@ -457,6 +502,7 @@ struct TmuxBinaryResolver: Sendable {
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
\(TmuxSocketEnvironment.powerShellPrelude(socketName: socketName))
$ghosthubProbeError = (& $ghosthubMux \(command) 2>&1 | Out-String)
$ghosthubProbeStatus = $LASTEXITCODE
if ($ghosthubProbeStatus -eq 0) {
Expand Down Expand Up @@ -574,24 +620,24 @@ struct TmuxBinaryResolver: Sendable {
guard line.hasPrefix(prefix) else { return nil }
let fields = line.split(
separator: "\t",
maxSplits: 9,
maxSplits: 10,
omittingEmptySubsequences: false
)
guard fields.count == 10,
let windowCount = Int(fields[1]),
guard fields.count == 11,
let windowCount = Int(fields[2]),
windowCount >= 0
else { return nil }
let activeWindowSize: TmuxGridSize? = if let columns = Int(fields[5]),
let rows = Int(fields[6]),
let activeWindowSize: TmuxGridSize? = if let columns = Int(fields[6]),
let rows = Int(fields[7]),
columns > 0, rows > 0 {
TmuxGridSize(columns: columns, rows: rows)
} else {
nil
}
let statusRows: Int? = switch fields[7] {
let statusRows: Int? = switch fields[8] {
case "off": 0
case "on": 1
default: Int(fields[7]).flatMap { $0 >= 0 ? $0 : nil }
default: Int(fields[8]).flatMap { $0 >= 0 ? $0 : nil }
}
let previewClientSize: TmuxGridSize? = activeWindowSize
.flatMap { size in
Expand All @@ -603,16 +649,24 @@ struct TmuxBinaryResolver: Sendable {
return TmuxGridSize(columns: size.columns, rows: rows)
}
return DiscoveredTmuxSession(
name: String(fields[9]),
name: String(fields[10]),
socketName: fields[1].isEmpty ? nil : String(fields[1]),
windowCount: windowCount,
serverPID: fields[2].isEmpty ? nil : String(fields[2]),
sessionID: fields[3].isEmpty ? nil : String(fields[3]),
createdAt: fields[4].isEmpty ? nil : String(fields[4]),
serverPID: fields[3].isEmpty ? nil : String(fields[3]),
sessionID: fields[4].isEmpty ? nil : String(fields[4]),
createdAt: fields[5].isEmpty ? nil : String(fields[5]),
activeWindowSize: activeWindowSize,
previewClientSize: previewClientSize,
managed: !fields[8].isEmpty
managed: !fields[9].isEmpty
)
}
let failurePrefix = kwtFailureMarker + "\t"
if let failure = result.stdout.split(whereSeparator: \.isNewline)
.last(where: { $0.hasPrefix(failurePrefix) }),
let status = Int32(failure.dropFirst(failurePrefix.count)),
status != 0 {
return .failure(.kwtDiscoveryFailed(sessions: sessions, status: status))
}
return .success(sessions)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/App/TmuxPaneSplitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ struct TmuxPaneSplitter: Sendable {
|| byte >= 97 && byte <= 122
})
else { return "exit 75" }
var arguments = [tmuxPath]
var arguments = TmuxSocketEnvironment.commandPrefix(socketName: socketName) + [tmuxPath]
if let socketName, !socketName.isEmpty {
arguments += ["-L", socketName]
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/App/TmuxSessionActivityProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ struct TmuxSessionActivityProbe: Sendable {
expectedIdentity: expectedIdentity
)
}
var baseArguments = [tmuxPath]
var baseArguments = TmuxSocketEnvironment.commandPrefix(socketName: selection.socketName)
+ [tmuxPath]
if let socketName = selection.socketName {
baseArguments.append(contentsOf: ["-L", socketName])
}
Expand Down Expand Up @@ -348,6 +349,7 @@ struct TmuxSessionActivityProbe: Sendable {
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [Console]::OutputEncoding
\(TmuxSocketEnvironment.powerShellPrelude(socketName: selection.socketName))
$ghosthubActivityVersionOutput = (\(version) 2>&1 | Out-String).Trim()
if (($LASTEXITCODE -ne 0) -or ($ghosthubActivityVersionOutput -notmatch '^tmux\\s+(\\d+)\\.(\\d+)(?:\\.(\\d+))?')) {
exit 69
Expand Down
33 changes: 19 additions & 14 deletions Sources/App/TmuxSessionKiller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,15 @@ struct TmuxSessionKiller: Sendable {
"kill-session -t \(expectedIdentity.sessionID)"
arguments[arguments.count - 1] =
"display-message -p \(identityMismatchMarker)"
return powerShellCommand(
arguments,
captureStandardError: true,
frameOutput: true
)
return TmuxSocketEnvironment
.powerShellPrelude(socketName: socketName) + powerShellCommand(
arguments,
captureStandardError: true,
frameOutput: true
)
}
return framedPOSIXCommand(arguments)
return framedPOSIXCommand(TmuxSocketEnvironment
.commandPrefix(socketName: socketName) + arguments)
}

private static func identityCommand(
Expand All @@ -410,9 +412,10 @@ struct TmuxSessionKiller: Sendable {
identityMarker + "#{pid}\t#{session_id}\t#{session_created}",
])
if platform == .windows {
return powerShellCommand(arguments)
return TmuxSocketEnvironment
.powerShellPrelude(socketName: socketName) + powerShellCommand(arguments)
}
return arguments
return (TmuxSocketEnvironment.commandPrefix(socketName: socketName) + arguments)
.map(shellQuotedCommandArgument)
.joined(separator: " ")
}
Expand All @@ -433,13 +436,15 @@ struct TmuxSessionKiller: Sendable {
"=\(sessionName):",
])
if platform == .windows {
return powerShellCommand(
arguments,
captureStandardError: true,
frameOutput: true
)
return TmuxSocketEnvironment
.powerShellPrelude(socketName: socketName) + powerShellCommand(
arguments,
captureStandardError: true,
frameOutput: true
)
}
return framedPOSIXCommand(arguments)
return framedPOSIXCommand(TmuxSocketEnvironment
.commandPrefix(socketName: socketName) + arguments)
}

static func isConfirmedAbsence(_ output: String) -> Bool {
Expand Down
7 changes: 5 additions & 2 deletions Sources/App/TmuxSessionProbeBroker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,11 @@ final class TmuxSessionProbeBroker {
) async -> TmuxSessionProbeOutcome {
guard target.socketName != nil else {
switch await sessions(on: .ssh(target.host)) {
case let .success(sessions):
return sessions.contains(where: { $0.name == target.name })
case let .success(sessions),
let .failure(.kwtDiscoveryFailed(sessions, _)):
return sessions
.contains(where: { $0.name == target.name && $0.socketName == target.socketName
})
? .present
: .absent
case let .failure(error):
Expand Down
Loading