Merge pull request #10101 from manaflow-ai/issue-10060-mosh-bootstrap-misreport
Fix mosh bootstrap staging: real argv for mosh-server, stage-specific errors, proxy address fallback
This commit is contained in:
@@ -78,6 +78,14 @@ extension CMUXCLI {
|
||||
remoteMoshProbeFailedMessage: String(
|
||||
localized: "cli.ssh.mosh.probeFailed",
|
||||
defaultValue: "[cmux] Could not verify remote Mosh support; continuing over SSH."
|
||||
),
|
||||
remoteBootstrapInstallFailedMessage: String(
|
||||
localized: "cli.ssh.mosh.bootstrapInstallFailed",
|
||||
defaultValue: "[cmux] Remote bootstrap install failed; continuing over SSH."
|
||||
),
|
||||
remoteMoshAddressFallbackMessage: String(
|
||||
localized: "cli.ssh.mosh.addressFallback",
|
||||
defaultValue: "[cmux] Remote SSH advertised an unusable address; resolving the Mosh address through the SSH connection."
|
||||
)
|
||||
).command()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ so call sites read naturally (`value.javaScriptStringLiteral`, not `f(value)`).
|
||||
- `String.javaScriptStringLiteral` — the string encoded as a quoted JavaScript string literal.
|
||||
- `SSHAgentSocketResolver` — OpenSSH option parsing and SSH agent socket path normalization.
|
||||
- `MoshTerminalCommandBuilder` — a pure Mosh startup-command builder with explicit SSH fallback.
|
||||
- `MoshRemoteIPMode` — the address-discovery mode selected for a Mosh connection.
|
||||
- `RemoteTmuxCommandBuilder` — shared remote `tmux` resolution and argv preservation.
|
||||
- `WorkspaceRemoteTerminalProfile` — durable shell-or-named-tmux terminal intent.
|
||||
- `WorkspaceRemoteTerminalTransport` — the persisted SSH-or-Mosh interactive terminal preference.
|
||||
@@ -51,7 +52,9 @@ let command = MoshTerminalCommandBuilder(
|
||||
localMoshMissingMessage: "Mosh is unavailable locally; using SSH.",
|
||||
localMoshUnsupportedMessage: "Mosh is too old for shared SSH setup; using SSH.",
|
||||
remoteMoshMissingMessage: "mosh-server is unavailable remotely; using SSH.",
|
||||
remoteMoshProbeFailedMessage: "Mosh capability check failed; using SSH."
|
||||
remoteMoshProbeFailedMessage: "Mosh capability check failed; using SSH.",
|
||||
remoteBootstrapInstallFailedMessage: "Remote bootstrap install failed; using SSH.",
|
||||
remoteMoshAddressFallbackMessage: "Remote SSH address is unusable; using local Mosh resolution."
|
||||
).command()
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Selects how Mosh discovers the address used for its UDP session.
|
||||
///
|
||||
/// Production callers always start from ``remote``; the launcher downgrades
|
||||
/// to ``proxy`` automatically when SSH advertises an unusable address. The
|
||||
/// other cases exist so tests can pin each generated mode explicitly.
|
||||
public enum MoshRemoteIPMode: String, Equatable, Sendable {
|
||||
/// Derive the address from the remote SSH connection when possible.
|
||||
case remote
|
||||
|
||||
/// Resolve the destination locally before starting the Mosh server.
|
||||
case local
|
||||
|
||||
/// Resolve the address through Mosh's local proxy path.
|
||||
case proxy
|
||||
}
|
||||
+81
-16
@@ -12,6 +12,7 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
private let destination: String
|
||||
private let remoteCommandArguments: [String]
|
||||
private let remoteRelayPort: Int?
|
||||
private let remoteIPMode: MoshRemoteIPMode
|
||||
private let preparationShellScript: String?
|
||||
private let managementReadyShellScript: String?
|
||||
private let sshFallbackCommand: String
|
||||
@@ -19,6 +20,8 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
private let localMoshUnsupportedMessage: String
|
||||
private let remoteMoshMissingMessage: String
|
||||
private let remoteMoshProbeFailedMessage: String
|
||||
private let remoteBootstrapInstallFailedMessage: String
|
||||
private let remoteMoshAddressFallbackMessage: String
|
||||
|
||||
/// Creates a Mosh terminal command builder.
|
||||
///
|
||||
@@ -29,13 +32,16 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
/// - destination: SSH destination or host alias.
|
||||
/// - remoteCommandArguments: Optional command argv launched by `mosh-server`.
|
||||
/// - remoteRelayPort: Optional remote relay whose presence enables authoritative lifecycle attempt registration.
|
||||
/// - preparationShellScript: Optional local preparation run after capability checks.
|
||||
/// - remoteIPMode: Address-discovery mode passed to Mosh; remote mode falls back to SSH proxy resolution when SSH advertises an unusable address.
|
||||
/// - preparationShellScript: Optional local preparation run before capability checks.
|
||||
/// - managementReadyShellScript: Optional local callback run after SSH preparation succeeds and before Mosh starts.
|
||||
/// - sshFallbackCommand: Complete local SSH terminal command used when Mosh is unavailable.
|
||||
/// - localMoshMissingMessage: User-facing message printed when no local `mosh` executable exists.
|
||||
/// - localMoshUnsupportedMessage: User-facing message printed when local Mosh lacks the required remote-IP mode.
|
||||
/// - remoteMoshMissingMessage: User-facing message printed when `mosh-server` is absent remotely.
|
||||
/// - remoteMoshProbeFailedMessage: User-facing message printed when the remote capability probe fails.
|
||||
/// - remoteBootstrapInstallFailedMessage: User-facing message printed when bootstrap staging fails.
|
||||
/// - remoteMoshAddressFallbackMessage: User-facing message printed when SSH proxy address resolution is selected automatically.
|
||||
public init(
|
||||
capabilityProbeSSHArguments: [String],
|
||||
sessionSSHArguments: [String],
|
||||
@@ -43,13 +49,16 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
destination: String,
|
||||
remoteCommandArguments: [String],
|
||||
remoteRelayPort: Int? = nil,
|
||||
remoteIPMode: MoshRemoteIPMode = .remote,
|
||||
preparationShellScript: String? = nil,
|
||||
managementReadyShellScript: String? = nil,
|
||||
sshFallbackCommand: String,
|
||||
localMoshMissingMessage: String,
|
||||
localMoshUnsupportedMessage: String,
|
||||
remoteMoshMissingMessage: String,
|
||||
remoteMoshProbeFailedMessage: String
|
||||
remoteMoshProbeFailedMessage: String,
|
||||
remoteBootstrapInstallFailedMessage: String,
|
||||
remoteMoshAddressFallbackMessage: String
|
||||
) {
|
||||
self.capabilityProbeSSHArguments = capabilityProbeSSHArguments
|
||||
self.sessionSSHArguments = sessionSSHArguments
|
||||
@@ -57,6 +66,7 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
self.destination = destination
|
||||
self.remoteCommandArguments = remoteCommandArguments
|
||||
self.remoteRelayPort = remoteRelayPort
|
||||
self.remoteIPMode = remoteIPMode
|
||||
self.preparationShellScript = preparationShellScript
|
||||
self.managementReadyShellScript = managementReadyShellScript
|
||||
self.sshFallbackCommand = sshFallbackCommand
|
||||
@@ -64,6 +74,8 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
self.localMoshUnsupportedMessage = localMoshUnsupportedMessage
|
||||
self.remoteMoshMissingMessage = remoteMoshMissingMessage
|
||||
self.remoteMoshProbeFailedMessage = remoteMoshProbeFailedMessage
|
||||
self.remoteBootstrapInstallFailedMessage = remoteBootstrapInstallFailedMessage
|
||||
self.remoteMoshAddressFallbackMessage = remoteMoshAddressFallbackMessage
|
||||
}
|
||||
|
||||
/// Returns a shell command that launches Mosh or falls back to SSH.
|
||||
@@ -93,11 +105,19 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
])
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
let remoteSSHConnectionScript = "printf '%s\\n' \"__CMUX_SSH_CONNECTION__${SSH_CONNECTION:-}\""
|
||||
let remoteSSHConnectionCommand = "/bin/sh -c \(remoteSSHConnectionScript.remoteCommandShellQuoted)"
|
||||
let remoteSSHConnectionProbe = (capabilityProbeSSHArguments + [
|
||||
"-T",
|
||||
destination,
|
||||
remoteSSHConnectionCommand,
|
||||
])
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
let moshSSHCommand = sessionSSHArguments
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
let moshArguments = ([
|
||||
"--experimental-remote-ip=remote",
|
||||
"--ssh=\(moshSSHCommand)",
|
||||
"--server=\(remoteMoshServerResolver.remoteExecPrefixShellCommand)",
|
||||
"--",
|
||||
@@ -123,17 +143,6 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
" ;;",
|
||||
"esac",
|
||||
"unset cmux_mosh_help",
|
||||
capabilityProbe,
|
||||
"cmux_mosh_probe_status=$?",
|
||||
"if [ \"$cmux_mosh_probe_status\" -eq 127 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshMissingMessage.remoteCommandShellQuoted) >&2",
|
||||
" cmux_mosh_fallback",
|
||||
"fi",
|
||||
"if [ \"$cmux_mosh_probe_status\" -ne 0 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshProbeFailedMessage.remoteCommandShellQuoted) >&2",
|
||||
" cmux_mosh_fallback",
|
||||
"fi",
|
||||
"unset cmux_mosh_probe_status",
|
||||
]
|
||||
let reportsTerminalLifecycle = remoteRelayPort.map { (1...65_535).contains($0) } ?? false
|
||||
if reportsTerminalLifecycle {
|
||||
@@ -146,12 +155,68 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
preparationShellScript,
|
||||
"cmux_mosh_prepare_status=$?",
|
||||
"if [ \"$cmux_mosh_prepare_status\" -ne 0 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshProbeFailedMessage.remoteCommandShellQuoted) >&2",
|
||||
" printf '%s\\n' \(remoteBootstrapInstallFailedMessage.remoteCommandShellQuoted) >&2",
|
||||
" cmux_mosh_fallback",
|
||||
"fi",
|
||||
"unset cmux_mosh_prepare_status cmux_remote_install_status",
|
||||
]
|
||||
}
|
||||
script += [
|
||||
"cmux_mosh_remote_ip_mode=\(remoteIPMode.rawValue.remoteCommandShellQuoted)",
|
||||
"cmux_mosh_address_fallback=0",
|
||||
]
|
||||
script += [
|
||||
capabilityProbe,
|
||||
"cmux_mosh_probe_status=$?",
|
||||
"if [ \"$cmux_mosh_probe_status\" -eq 127 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshMissingMessage.remoteCommandShellQuoted) >&2",
|
||||
" cmux_mosh_fallback",
|
||||
"fi",
|
||||
"if [ \"$cmux_mosh_probe_status\" -ne 0 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshProbeFailedMessage.remoteCommandShellQuoted) >&2",
|
||||
" cmux_mosh_fallback",
|
||||
"fi",
|
||||
]
|
||||
if remoteIPMode == .remote {
|
||||
// Mosh parses SSH_CONNECTION as four space-separated fields with
|
||||
// numeric ports and uses only the server address for its UDP
|
||||
// session, so validate exactly that shape and no more. When the
|
||||
// advertised address is unusable (empty, malformed, wildcard, or
|
||||
// loopback, which is what a port-forwarded SSH alias reports),
|
||||
// fall back to Mosh's proxy resolution: unlike local mode it
|
||||
// honors SSH aliases without requiring DNS on the destination.
|
||||
script += [
|
||||
"cmux_mosh_ssh_connection_probe_status=0",
|
||||
"cmux_mosh_ssh_connection_probe=\"$(\(remoteSSHConnectionProbe) 2>/dev/null)\" || cmux_mosh_ssh_connection_probe_status=$?",
|
||||
"case \"$cmux_mosh_ssh_connection_probe\" in *__CMUX_SSH_CONNECTION__*) cmux_mosh_ssh_connection=\"${cmux_mosh_ssh_connection_probe##*__CMUX_SSH_CONNECTION__}\" ;; *) cmux_mosh_ssh_connection= ;; esac",
|
||||
"if [ \"$cmux_mosh_ssh_connection_probe_status\" -ne 0 ] || [ -z \"$cmux_mosh_ssh_connection\" ]; then",
|
||||
" cmux_mosh_address_fallback=1",
|
||||
"else",
|
||||
" case \"$cmux_mosh_ssh_connection\" in",
|
||||
" *' '*' '*' '*)",
|
||||
" cmux_mosh_ssh_connection_tail=\"${cmux_mosh_ssh_connection#* }\"",
|
||||
" cmux_mosh_ssh_peer_port=\"${cmux_mosh_ssh_connection_tail%% *}\"",
|
||||
" cmux_mosh_ssh_connection_tail=\"${cmux_mosh_ssh_connection_tail#* }\"",
|
||||
" cmux_mosh_ssh_server_ip=\"${cmux_mosh_ssh_connection_tail%% *}\"",
|
||||
" cmux_mosh_ssh_connection_tail=\"${cmux_mosh_ssh_connection_tail#* }\"",
|
||||
" cmux_mosh_ssh_server_port=\"${cmux_mosh_ssh_connection_tail%% *}\"",
|
||||
" case \"$cmux_mosh_ssh_peer_port\" in ''|*[!0-9]*) cmux_mosh_address_fallback=1 ;; esac",
|
||||
" case \"$cmux_mosh_ssh_server_port\" in ''|*[!0-9]*) cmux_mosh_address_fallback=1 ;; esac",
|
||||
" case \"$cmux_mosh_ssh_server_ip\" in ''|0.0.0.0|::|::0|::1|127.*) cmux_mosh_address_fallback=1 ;; *[!0-9A-Fa-f:.]*) cmux_mosh_address_fallback=1 ;; esac",
|
||||
" ;;",
|
||||
" *) cmux_mosh_address_fallback=1 ;;",
|
||||
" esac",
|
||||
"fi",
|
||||
"if [ \"$cmux_mosh_address_fallback\" -eq 1 ]; then cmux_mosh_remote_ip_mode=proxy; fi",
|
||||
"unset cmux_mosh_ssh_connection_probe_status cmux_mosh_ssh_connection_probe cmux_mosh_ssh_connection cmux_mosh_ssh_peer_port cmux_mosh_ssh_connection_tail cmux_mosh_ssh_server_ip cmux_mosh_ssh_server_port",
|
||||
]
|
||||
}
|
||||
script += [
|
||||
"if [ \"$cmux_mosh_address_fallback\" -eq 1 ]; then",
|
||||
" printf '%s\\n' \(remoteMoshAddressFallbackMessage.remoteCommandShellQuoted) >&2",
|
||||
"fi",
|
||||
"unset cmux_mosh_probe_status cmux_mosh_address_fallback",
|
||||
]
|
||||
if let managementReadyShellScript = managementReadyShellScript?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!managementReadyShellScript.isEmpty {
|
||||
@@ -162,7 +227,7 @@ public struct MoshTerminalCommandBuilder: Sendable {
|
||||
}
|
||||
// Mosh exposes no reliable post-UDP-handshake callback, so this
|
||||
// pre-exec launcher must not claim authoritative connected readiness.
|
||||
script.append("exec \"$cmux_mosh\" \(moshArguments)")
|
||||
script.append("exec \"$cmux_mosh\" \"--experimental-remote-ip=$cmux_mosh_remote_ip_mode\" \(moshArguments)")
|
||||
return "/bin/sh -c \(script.joined(separator: "\n").remoteCommandShellQuoted)"
|
||||
}
|
||||
|
||||
|
||||
+34
-13
@@ -36,15 +36,12 @@ public struct RemoteBootstrapStagingCommandBuilder: Sendable {
|
||||
}
|
||||
|
||||
/// Local shell code that substitutes runtime IDs and streams the bootstrap over SSH.
|
||||
///
|
||||
/// The SSH command is one `/bin/sh -c` remote-command string so an account's
|
||||
/// configured login shell cannot parse the POSIX installer itself.
|
||||
public var preparationShellScript: String {
|
||||
let encodedBootstrapScript = Data(bootstrapScript.utf8).base64EncodedString()
|
||||
let installCommand = ([
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
remoteInstallShellScript,
|
||||
])
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
let installCommand = "/bin/sh -c \(remoteInstallShellScript.remoteCommandShellQuoted)"
|
||||
let sshPrefix = installerSSHArguments
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
@@ -61,21 +58,45 @@ public struct RemoteBootstrapStagingCommandBuilder: Sendable {
|
||||
"cmux_terminal_lifecycle_id_escaped=\"$(cmux_sed_escape \"$cmux_terminal_lifecycle_id\")\"",
|
||||
"cmux_ssh_attempt_id_escaped=\"$(cmux_sed_escape \"$cmux_ssh_attempt_id\")\"",
|
||||
"cmux_remote_bootstrap=\"$(printf '%s' \"$cmux_remote_bootstrap\" | sed \"s/__CMUX_WORKSPACE_ID__/$cmux_workspace_id_escaped/g; s/__CMUX_SURFACE_ID__/$cmux_surface_id_escaped/g; s/__CMUX_TERMINAL_LIFECYCLE_ID__/$cmux_terminal_lifecycle_id_escaped/g; s/__CMUX_SSH_ATTEMPT_ID__/$cmux_ssh_attempt_id_escaped/g\")\"",
|
||||
"printf '%s' \"$cmux_remote_bootstrap\" | command \(sshPrefix) -T \(destination.remoteCommandShellQuoted) \(installCommand.remoteCommandShellQuoted)",
|
||||
"cmux_remote_install_status=$?",
|
||||
"unset cmux_remote_bootstrap cmux_remote_bootstrap_b64 cmux_workspace_id cmux_surface_id cmux_terminal_lifecycle_id cmux_ssh_attempt_id cmux_workspace_id_escaped cmux_surface_id_escaped cmux_terminal_lifecycle_id_escaped cmux_ssh_attempt_id_escaped",
|
||||
"cmux_remote_install_stderr_file=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-remote-bootstrap-install.XXXXXX\" 2>/dev/null || true)\"",
|
||||
"if [ -n \"$cmux_remote_install_stderr_file\" ]; then",
|
||||
" printf '%s' \"$cmux_remote_bootstrap\" | command \(sshPrefix) -T \(destination.remoteCommandShellQuoted) \(installCommand.remoteCommandShellQuoted) 2>\"$cmux_remote_install_stderr_file\"",
|
||||
" cmux_remote_install_status=$?",
|
||||
"else",
|
||||
" printf '%s' \"$cmux_remote_bootstrap\" | command \(sshPrefix) -T \(destination.remoteCommandShellQuoted) \(installCommand.remoteCommandShellQuoted)",
|
||||
" cmux_remote_install_status=$?",
|
||||
"fi",
|
||||
"if [ \"$cmux_remote_install_status\" -ne 0 ] && [ -n \"$cmux_remote_install_stderr_file\" ] && [ -s \"$cmux_remote_install_stderr_file\" ]; then",
|
||||
" cat \"$cmux_remote_install_stderr_file\" >&2",
|
||||
"fi",
|
||||
"rm -f -- \"${cmux_remote_install_stderr_file:-}\" 2>/dev/null || true",
|
||||
"unset cmux_remote_bootstrap cmux_remote_bootstrap_b64 cmux_workspace_id cmux_surface_id cmux_terminal_lifecycle_id cmux_ssh_attempt_id cmux_workspace_id_escaped cmux_surface_id_escaped cmux_terminal_lifecycle_id_escaped cmux_ssh_attempt_id_escaped cmux_remote_install_stderr_file",
|
||||
"(exit \"$cmux_remote_install_status\")",
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Small remote shell command that replaces the process with the staged bootstrap.
|
||||
///
|
||||
/// OpenSSH hands its remote command to the account's configured login
|
||||
/// shell first. Quote the POSIX launcher as the argument to an explicit
|
||||
/// `/bin/sh -c` so fish/csh/nushell never parse the staged bootstrap
|
||||
/// command themselves.
|
||||
public var remoteExecutionShellScript: String {
|
||||
"exec /bin/sh \"$HOME/.cmux/relay/\(remoteRelayPort).bootstrap.sh\""
|
||||
"/bin/sh -c \(stagedBootstrapLauncherScript.remoteCommandShellQuoted)"
|
||||
}
|
||||
|
||||
/// Remote argv that executes the staged bootstrap.
|
||||
/// Remote command argv that executes the staged bootstrap.
|
||||
///
|
||||
/// Mosh forwards this argv to `mosh-server`, which executes it with
|
||||
/// `execvp` and no shell parsing, so the launcher must be real argv
|
||||
/// elements: a single command string would be treated as a literal
|
||||
/// executable pathname and fail.
|
||||
public var remoteExecutionCommandArguments: [String] {
|
||||
["/bin/sh", "-c", remoteExecutionShellScript]
|
||||
["/bin/sh", "-c", stagedBootstrapLauncherScript]
|
||||
}
|
||||
|
||||
private var stagedBootstrapLauncherScript: String {
|
||||
"exec /bin/sh \"$HOME/.cmux/relay/\(remoteRelayPort).bootstrap.sh\""
|
||||
}
|
||||
|
||||
private var remoteInstallShellScript: String {
|
||||
|
||||
+20
-23
@@ -37,11 +37,15 @@ public struct RemoteExecutableCommandBuilder: Sendable {
|
||||
/// Returns a shell-quoted remote command that resolves and executes argv.
|
||||
///
|
||||
/// - Parameter arguments: Arguments forwarded to the resolved executable.
|
||||
///
|
||||
/// The returned value is a single `/bin/sh -c` command string. This keeps
|
||||
/// resolver assignments away from a host-configured fish/csh login shell.
|
||||
/// - Returns: A command suitable for an OpenSSH remote-command string.
|
||||
public func remoteShellCommand(arguments: [String]) -> String {
|
||||
remoteCommandArguments(arguments: arguments)
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
Self.remoteShellCommand(
|
||||
script: Self.executionShellScript,
|
||||
arguments: ["cmux-remote-executable", executableName, notFoundSentinel] + arguments
|
||||
)
|
||||
}
|
||||
|
||||
/// A shell command that prints the resolved executable path.
|
||||
@@ -49,16 +53,10 @@ public struct RemoteExecutableCommandBuilder: Sendable {
|
||||
/// The command exits 127 and emits the configured sentinel when no
|
||||
/// executable can be found.
|
||||
public var resolutionProbeShellCommand: String {
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
Self.resolutionShellScript,
|
||||
"cmux-remote-executable",
|
||||
executableName,
|
||||
notFoundSentinel,
|
||||
]
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
Self.remoteShellCommand(
|
||||
script: Self.resolutionShellScript,
|
||||
arguments: ["cmux-remote-executable", executableName, notFoundSentinel]
|
||||
)
|
||||
}
|
||||
|
||||
/// A command prefix to which a remote launcher may append arguments.
|
||||
@@ -67,16 +65,10 @@ public struct RemoteExecutableCommandBuilder: Sendable {
|
||||
/// those arguments become the resolver's argv and reach the discovered
|
||||
/// server executable unchanged.
|
||||
public var remoteExecPrefixShellCommand: String {
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
Self.executionShellScript,
|
||||
"cmux-remote-executable",
|
||||
executableName,
|
||||
notFoundSentinel,
|
||||
]
|
||||
.map(\.remoteCommandShellQuoted)
|
||||
.joined(separator: " ")
|
||||
Self.remoteShellCommand(
|
||||
script: Self.executionShellScript,
|
||||
arguments: ["cmux-remote-executable", executableName, notFoundSentinel]
|
||||
)
|
||||
}
|
||||
|
||||
private static let resolutionShellScript =
|
||||
@@ -102,6 +94,11 @@ public struct RemoteExecutableCommandBuilder: Sendable {
|
||||
"eval \"$(/usr/libexec/path_helper -s 2>/dev/null)\"; " +
|
||||
"if command -v \"$cmux_executable_name\" >/dev/null 2>&1; then " +
|
||||
"cmux_executable_path=\"$(command -v \"$cmux_executable_name\")\"; fi; fi; "
|
||||
|
||||
private static func remoteShellCommand(script: String, arguments: [String]) -> String {
|
||||
let quotedArguments = arguments.map(\.remoteCommandShellQuoted).joined(separator: " ")
|
||||
return "/bin/sh -c \(script.remoteCommandShellQuoted) \(quotedArguments)"
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
+298
-14
@@ -81,6 +81,43 @@ struct MoshTerminalCommandBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"runs remote probes through POSIX sh under a fish login shell",
|
||||
.enabled(if: MoshTerminalCommandBuilderTests.fishExecutablePath != nil)
|
||||
)
|
||||
func remoteProbeIsShellAgnostic() throws {
|
||||
let fishPath = try #require(Self.fishExecutablePath)
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
executeRemoteCommand: true,
|
||||
installRemoteMoshServerOutsidePath: true,
|
||||
remoteLoginShell: fishPath
|
||||
) { directory, environment in
|
||||
let remoteHome = directory.appendingPathComponent("remote-home", isDirectory: true)
|
||||
let staging = try #require(RemoteBootstrapStagingCommandBuilder(
|
||||
installerSSHArguments: ["ssh", "-o", "RemoteCommand=none"],
|
||||
destination: "[email protected]",
|
||||
remoteRelayPort: 52_263,
|
||||
bootstrapScript: "printf '%s\\n' fish-bootstrap"
|
||||
))
|
||||
let result = try run(
|
||||
builder(
|
||||
preparationShellScript: staging.preparationShellScript,
|
||||
remoteRelayPort: 52_263
|
||||
),
|
||||
environment: environment
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(result.stdout.isEmpty)
|
||||
#expect(result.stderr.isEmpty)
|
||||
#expect(FileManager.default.fileExists(atPath: directory.appendingPathComponent("mosh.args").path))
|
||||
#expect(FileManager.default.fileExists(
|
||||
atPath: remoteHome.appendingPathComponent(".cmux/relay/52263.bootstrap.sh").path
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("preserves the Mosh SSH bootstrap and remote command argv")
|
||||
func supportedMoshPreservesArguments() throws {
|
||||
try withFakeCommands(sshStatus: 0) { directory, environment in
|
||||
@@ -89,10 +126,18 @@ struct MoshTerminalCommandBuilderTests {
|
||||
decoding: try Data(contentsOf: directory.appendingPathComponent("mosh.args")),
|
||||
as: UTF8.self
|
||||
).split(separator: "\n", omittingEmptySubsequences: false).dropLast().map(String.init)
|
||||
let probeArguments = String(
|
||||
let probeOutput = String(
|
||||
decoding: try Data(contentsOf: directory.appendingPathComponent("ssh.args")),
|
||||
as: UTF8.self
|
||||
).split(separator: "\n", omittingEmptySubsequences: false).dropLast().map(String.init)
|
||||
)
|
||||
let probeInvocations = probeOutput
|
||||
.components(separatedBy: "__CMUX_SSH_INVOCATION_END__\n")
|
||||
.filter { !$0.isEmpty }
|
||||
.map { $0.split(separator: "\n").map(String.init) }
|
||||
let capabilityProbeArguments = try #require(
|
||||
probeInvocations.first(where: { $0.last?.contains("mosh-server") == true })
|
||||
)
|
||||
let probeArguments = try #require(probeInvocations.first)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(result.stdout.isEmpty)
|
||||
@@ -100,8 +145,8 @@ struct MoshTerminalCommandBuilderTests {
|
||||
#expect(Array(probeArguments.prefix(4)) == [
|
||||
"-o", "RemoteCommand=none", "-T", "[email protected]",
|
||||
])
|
||||
#expect(probeArguments.last?.contains("mosh-server") == true)
|
||||
#expect(probeArguments.last?.contains("$HOME/.local/bin") == true)
|
||||
#expect(capabilityProbeArguments.last?.contains("$HOME/.local/bin") == true)
|
||||
#expect(probeInvocations.contains(where: { $0.last?.contains("SSH_CONNECTION") == true }))
|
||||
#expect(moshArguments[0] == "--experimental-remote-ip=remote")
|
||||
#expect(moshArguments[1] == "--ssh='ssh' '-o' 'RemoteCommand=none' '-p' '2222'")
|
||||
#expect(moshArguments[2].hasPrefix("--server="))
|
||||
@@ -159,14 +204,177 @@ struct MoshTerminalCommandBuilderTests {
|
||||
let result = try run(
|
||||
builder(
|
||||
sshFallbackCommand: "printf 'ssh fallback\\n'",
|
||||
preparationShellScript: "false"
|
||||
preparationShellScript: "printf '%s\\n' 'bootstrap install stderr' >&2; false"
|
||||
),
|
||||
environment: environment
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(result.stdout == "ssh fallback\n")
|
||||
#expect(result.stderr == "remote probe failed\n")
|
||||
#expect(result.stderr.contains("remote bootstrap install failed"))
|
||||
#expect(result.stderr.contains("bootstrap install stderr"))
|
||||
#expect(!result.stderr.contains("remote probe failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when SSH advertises an unusable address")
|
||||
func unusableRemoteAddressUsesProxyMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "0.0.0.0 0 0.0.0.0 0"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when SSH_CONNECTION is empty")
|
||||
func emptyRemoteAddressUsesProxyMode() throws {
|
||||
try withFakeCommands(sshStatus: 0, sshConnection: "") { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when SSH advertises a loopback server address")
|
||||
func loopbackServerAddressUsesProxyMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "127.0.0.1 51675 127.0.0.1 22"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when SSH_CONNECTION is truncated")
|
||||
func truncatedRemoteAddressUsesProxyMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "192.0.2.10 12345"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when a port is non-numeric")
|
||||
func nonNumericPortUsesProxyMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "192.0.2.10 12345 192.0.2.20 ssh"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("falls back to SSH proxy address resolution when the SSH address probe fails")
|
||||
func addressProbeFailureUsesProxyMode() throws {
|
||||
try withFakeCommands(sshStatus: 0, sshConnectionStatus: 255) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=proxy")
|
||||
#expect(result.stderr.contains("mosh address fallback engaged"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("keeps remote address resolution when SSH advertises zero ports")
|
||||
func zeroRemotePortsKeepRemoteMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "192.0.2.10 0 192.0.2.20 0"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=remote")
|
||||
#expect(result.stderr.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("keeps remote address resolution when only the peer address is unusual")
|
||||
func unusualPeerAddressKeepsRemoteMode() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
sshConnection: "fe80::1%en0 51675 203.0.113.7 22"
|
||||
) { directory, environment in
|
||||
let result = try run(builder(), environment: environment)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=remote")
|
||||
#expect(result.stderr.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("honors an explicit local Mosh address mode")
|
||||
func explicitLocalMode() throws {
|
||||
try withFakeCommands(sshStatus: 0) { directory, environment in
|
||||
let result = try run(
|
||||
builder(remoteIPMode: .local),
|
||||
environment: environment
|
||||
)
|
||||
let moshArguments = try String(
|
||||
contentsOf: directory.appendingPathComponent("mosh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
#expect(result.status == 0)
|
||||
#expect(moshArguments.firstLine == "--experimental-remote-ip=local")
|
||||
#expect(result.stderr.isEmpty)
|
||||
let probeOutput = try String(
|
||||
contentsOf: directory.appendingPathComponent("ssh.args"),
|
||||
encoding: .utf8
|
||||
)
|
||||
#expect(!probeOutput.contains("SSH_CONNECTION"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +424,38 @@ struct MoshTerminalCommandBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("runs the staged bootstrap through mosh-server execvp argv semantics")
|
||||
func stagedBootstrapSurvivesMoshServerExec() throws {
|
||||
try withFakeCommands(
|
||||
sshStatus: 0,
|
||||
executeRemoteCommand: true,
|
||||
installRemoteMoshServerOutsidePath: true,
|
||||
moshExecutesRemoteCommand: true
|
||||
) { directory, environment in
|
||||
let remoteHome = directory.appendingPathComponent("remote-home", isDirectory: true)
|
||||
let staging = try #require(RemoteBootstrapStagingCommandBuilder(
|
||||
installerSSHArguments: ["ssh", "-o", "RemoteCommand=none"],
|
||||
destination: "[email protected]",
|
||||
remoteRelayPort: 52_264,
|
||||
bootstrapScript: "printf ran > \"$HOME/bootstrap-ran\""
|
||||
))
|
||||
let result = try run(
|
||||
builder(
|
||||
preparationShellScript: staging.preparationShellScript,
|
||||
remoteRelayPort: 52_264,
|
||||
remoteCommandArguments: staging.remoteExecutionCommandArguments
|
||||
),
|
||||
environment: environment
|
||||
)
|
||||
|
||||
#expect(result.status == 0, "stderr: \(result.stderr)")
|
||||
#expect(result.stderr.isEmpty)
|
||||
#expect(FileManager.default.fileExists(
|
||||
atPath: remoteHome.appendingPathComponent("bootstrap-ran").path
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("does not report connected before the Mosh transport establishes")
|
||||
func failedMoshDoesNotReportConnected() throws {
|
||||
try withFakeCommands(sshStatus: 0, moshStatus: 71) { directory, environment in
|
||||
@@ -243,32 +483,49 @@ struct MoshTerminalCommandBuilderTests {
|
||||
preparationShellScript: String? = nil,
|
||||
managementReadyShellScript: String? = nil,
|
||||
remoteRelayPort: Int? = nil,
|
||||
localMoshExecutableName: String = "mosh"
|
||||
remoteIPMode: MoshRemoteIPMode = .remote,
|
||||
localMoshExecutableName: String = "mosh",
|
||||
remoteCommandArguments: [String] = ["command", "space arg", "quote'arg"]
|
||||
) -> MoshTerminalCommandBuilder {
|
||||
MoshTerminalCommandBuilder(
|
||||
capabilityProbeSSHArguments: ["ssh", "-o", "RemoteCommand=none"],
|
||||
sessionSSHArguments: ["ssh", "-o", "RemoteCommand=none", "-p", "2222"],
|
||||
localMoshExecutableName: localMoshExecutableName,
|
||||
destination: "[email protected]",
|
||||
remoteCommandArguments: ["command", "space arg", "quote'arg"],
|
||||
remoteCommandArguments: remoteCommandArguments,
|
||||
remoteRelayPort: remoteRelayPort,
|
||||
remoteIPMode: remoteIPMode,
|
||||
preparationShellScript: preparationShellScript,
|
||||
managementReadyShellScript: managementReadyShellScript,
|
||||
sshFallbackCommand: sshFallbackCommand,
|
||||
localMoshMissingMessage: "local mosh missing",
|
||||
localMoshUnsupportedMessage: "local mosh unsupported",
|
||||
remoteMoshMissingMessage: "remote mosh missing",
|
||||
remoteMoshProbeFailedMessage: "remote probe failed"
|
||||
remoteMoshProbeFailedMessage: "remote probe failed",
|
||||
remoteBootstrapInstallFailedMessage: "remote bootstrap install failed",
|
||||
remoteMoshAddressFallbackMessage: "mosh address fallback engaged"
|
||||
)
|
||||
}
|
||||
|
||||
private static var fishExecutablePath: String? {
|
||||
[
|
||||
"/opt/homebrew/bin/fish",
|
||||
"/usr/local/bin/fish",
|
||||
"/usr/bin/fish",
|
||||
].first(where: { FileManager.default.isExecutableFile(atPath: $0) })
|
||||
}
|
||||
|
||||
private func withFakeCommands(
|
||||
sshStatus: Int32,
|
||||
installMosh: Bool = true,
|
||||
moshSupportsRemoteIP: Bool = true,
|
||||
executeRemoteCommand: Bool = false,
|
||||
installRemoteMoshServerOutsidePath: Bool = false,
|
||||
moshExecutesRemoteCommand: Bool = false,
|
||||
requireManagementReady: Bool = false,
|
||||
sshConnection: String? = nil,
|
||||
sshConnectionStatus: Int32? = nil,
|
||||
remoteLoginShell: String = "/bin/sh",
|
||||
moshStatus: Int32 = 0,
|
||||
operation: (URL, [String: String]) throws -> Void
|
||||
) throws {
|
||||
@@ -281,13 +538,22 @@ struct MoshTerminalCommandBuilderTests {
|
||||
named: "ssh",
|
||||
script: """
|
||||
#!/bin/sh
|
||||
printf '%s\\n' "$@" > "$SSH_ARGS_FILE"
|
||||
printf '%s\\n' "$@" >> "$SSH_ARGS_FILE"
|
||||
printf '%s\\n' '__CMUX_SSH_INVOCATION_END__' >> "$SSH_ARGS_FILE"
|
||||
cmux_remote_command=
|
||||
for cmux_arg in "$@"; do cmux_remote_command=$cmux_arg; done
|
||||
if [ "$FAKE_SSH_EXEC_REMOTE" = "1" ]; then
|
||||
cmux_remote_command=
|
||||
for cmux_arg in "$@"; do cmux_remote_command=$cmux_arg; done
|
||||
HOME="$FAKE_REMOTE_HOME" PATH=/usr/bin:/bin /bin/sh -c "$cmux_remote_command"
|
||||
SSH_CONNECTION="$FAKE_SSH_CONNECTION" HOME="$FAKE_REMOTE_HOME" PATH=/usr/bin:/bin "$FAKE_REMOTE_LOGIN_SHELL" -c "$cmux_remote_command"
|
||||
exit $?
|
||||
fi
|
||||
case "$cmux_remote_command" in
|
||||
*SSH_CONNECTION*)
|
||||
if [ -n "${FAKE_SSH_CONNECTION_STATUS:-}" ]; then
|
||||
exit "$FAKE_SSH_CONNECTION_STATUS"
|
||||
fi
|
||||
printf '%s\\n' "__CMUX_SSH_CONNECTION__${FAKE_SSH_CONNECTION:-}"
|
||||
;;
|
||||
esac
|
||||
exit "$FAKE_SSH_STATUS"
|
||||
""",
|
||||
in: directory
|
||||
@@ -316,6 +582,14 @@ struct MoshTerminalCommandBuilderTests {
|
||||
exit 71
|
||||
fi
|
||||
printf '%s\\n' "$@" > "$MOSH_ARGS_FILE"
|
||||
if [ "$FAKE_MOSH_EXECS_REMOTE_COMMAND" = "1" ]; then
|
||||
while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done
|
||||
if [ "$#" -gt 0 ]; then shift; fi
|
||||
if [ "$#" -gt 0 ]; then shift; fi
|
||||
if [ "$#" -gt 0 ]; then
|
||||
HOME="$FAKE_REMOTE_HOME" exec "$@"
|
||||
fi
|
||||
fi
|
||||
exit "$FAKE_MOSH_STATUS"
|
||||
""",
|
||||
in: directory
|
||||
@@ -335,11 +609,15 @@ struct MoshTerminalCommandBuilderTests {
|
||||
)
|
||||
}
|
||||
try operation(directory, [
|
||||
"PATH": directory.path,
|
||||
"PATH": directory.path + ":/usr/bin:/bin",
|
||||
"FAKE_SSH_STATUS": String(sshStatus),
|
||||
"FAKE_SSH_EXEC_REMOTE": executeRemoteCommand ? "1" : "0",
|
||||
"FAKE_REMOTE_HOME": remoteHome.path,
|
||||
"FAKE_REMOTE_LOGIN_SHELL": remoteLoginShell,
|
||||
"FAKE_SSH_CONNECTION": sshConnection ?? "192.0.2.10 12345 192.0.2.20 22",
|
||||
"FAKE_SSH_CONNECTION_STATUS": sshConnectionStatus.map(String.init) ?? "",
|
||||
"FAKE_MOSH_SUPPORTS_REMOTE_IP": moshSupportsRemoteIP ? "1" : "0",
|
||||
"FAKE_MOSH_EXECS_REMOTE_COMMAND": moshExecutesRemoteCommand ? "1" : "0",
|
||||
"FAKE_REQUIRE_MANAGEMENT_READY": requireManagementReady ? "1" : "0",
|
||||
"FAKE_MOSH_STATUS": String(moshStatus),
|
||||
"MANAGEMENT_READY_FILE": directory.appendingPathComponent("management.ready").path,
|
||||
@@ -381,3 +659,9 @@ struct MoshTerminalCommandBuilderTests {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var firstLine: String {
|
||||
split(whereSeparator: \.isNewline).first.map(String.init) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
+146
@@ -72,6 +72,7 @@ struct RemoteBootstrapStagingCommandBuilderTests {
|
||||
.split(separator: "\n")
|
||||
.map(String.init)
|
||||
#expect(sshArguments.allSatisfy { $0.utf8.count < 4_096 })
|
||||
#expect(sshArguments.contains(where: { $0.hasPrefix("/bin/sh -c '") }))
|
||||
|
||||
let execution = try run(
|
||||
executable: "/bin/sh",
|
||||
@@ -89,6 +90,143 @@ struct RemoteBootstrapStagingCommandBuilderTests {
|
||||
#expect(execution.stderr.isEmpty)
|
||||
}
|
||||
|
||||
@Test("execution argv runs the staged bootstrap under execvp semantics")
|
||||
func executionArgumentsSurviveExecvp() throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("cmux-bootstrap-argv-\(UUID().uuidString)", isDirectory: true)
|
||||
let remoteHome = directory.appendingPathComponent("remote-home", isDirectory: true)
|
||||
let relayDirectory = remoteHome.appendingPathComponent(".cmux/relay", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: relayDirectory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
try "printf '%s\\n' argv-bootstrap\n".write(
|
||||
to: relayDirectory.appendingPathComponent("52264.bootstrap.sh"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
let builder = try #require(RemoteBootstrapStagingCommandBuilder(
|
||||
installerSSHArguments: ["ssh"],
|
||||
destination: "[email protected]",
|
||||
remoteRelayPort: 52_264,
|
||||
bootstrapScript: "printf '%s\\n' argv-bootstrap"
|
||||
))
|
||||
|
||||
// mosh-server executes the received command argv with execvp and no
|
||||
// shell parsing, so the first element must be a real executable path.
|
||||
let arguments = builder.remoteExecutionCommandArguments
|
||||
let execution = try run(
|
||||
executable: try #require(arguments.first),
|
||||
arguments: Array(arguments.dropFirst()),
|
||||
environment: [
|
||||
"HOME": remoteHome.path,
|
||||
"PATH": "/usr/bin:/bin",
|
||||
]
|
||||
)
|
||||
#expect(execution.status == 0)
|
||||
#expect(execution.stdout == "argv-bootstrap\n")
|
||||
#expect(execution.stderr.isEmpty)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"installs through POSIX sh when the remote login shell is fish",
|
||||
.enabled(if: RemoteBootstrapStagingCommandBuilderTests.fishExecutablePath != nil)
|
||||
)
|
||||
func stagesThroughFishLoginShell() throws {
|
||||
let fishPath = try #require(Self.fishExecutablePath)
|
||||
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("cmux-bootstrap-fish-\(UUID().uuidString)", isDirectory: true)
|
||||
let remoteHome = directory.appendingPathComponent("remote-home", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: remoteHome, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let fakeSSH = directory.appendingPathComponent("ssh")
|
||||
try """
|
||||
#!/bin/sh
|
||||
cmux_remote_command=
|
||||
for cmux_argument in "$@"; do cmux_remote_command=$cmux_argument; done
|
||||
HOME="$CMUX_REMOTE_HOME" PATH=/usr/bin:/bin "$CMUX_FISH_PATH" -c "$cmux_remote_command"
|
||||
""".write(to: fakeSSH, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes(
|
||||
[.posixPermissions: 0o755],
|
||||
ofItemAtPath: fakeSSH.path
|
||||
)
|
||||
|
||||
let builder = try #require(RemoteBootstrapStagingCommandBuilder(
|
||||
installerSSHArguments: [fakeSSH.path, "-o", "RemoteCommand=none"],
|
||||
destination: "[email protected]",
|
||||
remoteRelayPort: 52_262,
|
||||
bootstrapScript: "printf '%s\\n' fish-bootstrap"
|
||||
))
|
||||
let preparation = try run(
|
||||
executable: "/bin/sh",
|
||||
arguments: ["-c", builder.preparationShellScript],
|
||||
environment: [
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"CMUX_REMOTE_HOME": remoteHome.path,
|
||||
"CMUX_FISH_PATH": fishPath,
|
||||
]
|
||||
)
|
||||
|
||||
#expect(preparation.status == 0)
|
||||
#expect(preparation.stderr.isEmpty)
|
||||
#expect(FileManager.default.fileExists(
|
||||
atPath: remoteHome.appendingPathComponent(".cmux/relay/52262.bootstrap.sh").path
|
||||
))
|
||||
|
||||
let execution = try run(
|
||||
executable: fishPath,
|
||||
arguments: ["-c", builder.remoteExecutionShellScript],
|
||||
environment: [
|
||||
"HOME": remoteHome.path,
|
||||
"PATH": "/usr/bin:/bin",
|
||||
]
|
||||
)
|
||||
#expect(execution.status == 0)
|
||||
#expect(execution.stdout == "fish-bootstrap\n")
|
||||
#expect(execution.stderr.isEmpty)
|
||||
}
|
||||
|
||||
@Test("returns installer status and captured stderr")
|
||||
func reportsInstallerFailure() throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("cmux-bootstrap-failure-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let fakeSSH = directory.appendingPathComponent("ssh")
|
||||
try """
|
||||
#!/bin/sh
|
||||
cat >/dev/null
|
||||
printf '%s\\n' 'remote installer stderr' >&2
|
||||
exit 23
|
||||
""".write(to: fakeSSH, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes(
|
||||
[.posixPermissions: 0o755],
|
||||
ofItemAtPath: fakeSSH.path
|
||||
)
|
||||
|
||||
let builder = try #require(RemoteBootstrapStagingCommandBuilder(
|
||||
installerSSHArguments: [fakeSSH.path, "-o", "RemoteCommand=none"],
|
||||
destination: "[email protected]",
|
||||
remoteRelayPort: 52_263,
|
||||
bootstrapScript: "true"
|
||||
))
|
||||
let preparation = try run(
|
||||
executable: "/bin/sh",
|
||||
arguments: ["-c", builder.preparationShellScript],
|
||||
environment: [
|
||||
"PATH": "/usr/bin:/bin",
|
||||
]
|
||||
)
|
||||
|
||||
#expect(preparation.status == 23)
|
||||
#expect(preparation.stderr.contains("remote installer stderr"))
|
||||
#expect(!FileManager.default.fileExists(
|
||||
atPath: directory.appendingPathComponent("remote-home/.cmux/relay/52263.bootstrap.sh").path
|
||||
))
|
||||
}
|
||||
|
||||
@Test("rejects an invalid relay namespace")
|
||||
func invalidRelayPort() {
|
||||
#expect(RemoteBootstrapStagingCommandBuilder(
|
||||
@@ -120,4 +258,12 @@ struct RemoteBootstrapStagingCommandBuilderTests {
|
||||
String(decoding: standardError.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
|
||||
)
|
||||
}
|
||||
|
||||
private static var fishExecutablePath: String? {
|
||||
[
|
||||
"/opt/homebrew/bin/fish",
|
||||
"/usr/local/bin/fish",
|
||||
"/usr/bin/fish",
|
||||
].first(where: { FileManager.default.isExecutableFile(atPath: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54327,6 +54327,256 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli.ssh.mosh.bootstrapInstallFailed": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] فشل تثبيت التمهيد البعيد؛ جارٍ المتابعة عبر SSH."
|
||||
}
|
||||
},
|
||||
"bs": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Instalacija udaljenog pokretanja nije uspjela; nastavak putem SSH-a."
|
||||
}
|
||||
},
|
||||
"da": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Installation af fjern-bootstrap mislykkedes; fortsætter via SSH."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Installation des Remote-Bootstraps fehlgeschlagen; Verbindung wird über SSH fortgesetzt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Remote bootstrap install failed; continuing over SSH."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Falló la instalación del arranque remoto; se continuará mediante SSH."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Échec de l’installation du bootstrap distant ; poursuite via SSH."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Installazione del bootstrap remoto non riuscita; continuazione tramite SSH."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] リモートブートストラップのインストールに失敗したため、SSH で接続を続行します。"
|
||||
}
|
||||
},
|
||||
"km": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] ការដំឡើង bootstrap ពីចម្ងាយបានបរាជ័យ ដូច្នេះបន្តតាម SSH។"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 원격 부트스트랩 설치에 실패하여 SSH로 계속합니다."
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Installasjon av ekstern bootstrap mislyktes; fortsetter via SSH."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Instalacja zdalnego bootstrapu nie powiodła się; kontynuowanie przez SSH."
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Falha ao instalar o bootstrap remoto; continuando por SSH."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Не удалось установить удалённый bootstrap; продолжение через SSH."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] ติดตั้ง bootstrap ระยะไกลไม่สำเร็จ จึงดำเนินการต่อผ่าน SSH"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Uzak bootstrap kurulamadı; SSH üzerinden devam ediliyor."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Не вдалося встановити віддалений bootstrap; продовження через SSH."
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 远程引导安装失败;将继续使用 SSH。"
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 遠端 bootstrap 安裝失敗;將繼續使用 SSH。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli.ssh.mosh.addressFallback": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] أعلن SSH البعيد عن عنوان غير صالح؛ سيتم تحديد عنوان Mosh عبر اتصال SSH."
|
||||
}
|
||||
},
|
||||
"bs": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Udaljeni SSH je prijavio neupotrebljivu adresu; Mosh adresa se određuje kroz SSH vezu."
|
||||
}
|
||||
},
|
||||
"da": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Fjern-SSH annoncerede en ugyldig adresse; Mosh-adressen findes via SSH-forbindelsen."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Der Remote-SSH-Dienst meldete eine unbrauchbare Adresse; die Mosh-Adresse wird über die SSH-Verbindung ermittelt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Remote SSH advertised an unusable address; resolving the Mosh address through the SSH connection."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] El SSH remoto anunció una dirección no utilizable; la dirección de Mosh se resolverá a través de la conexión SSH."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Le SSH distant a annoncé une adresse inutilisable ; l’adresse Mosh est résolue via la connexion SSH."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] SSH remoto ha annunciato un indirizzo inutilizzabile; l’indirizzo Mosh viene risolto tramite la connessione SSH."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] リモート SSH が使用できないアドレスを通知したため、SSH 接続経由で Mosh のアドレスを解決します。"
|
||||
}
|
||||
},
|
||||
"km": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] SSH ពីចម្ងាយបានប្រកាសអាសយដ្ឋានដែលមិនអាចប្រើបាន ដូច្នេះដោះស្រាយអាសយដ្ឋាន Mosh តាមរយៈការតភ្ជាប់ SSH។"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 원격 SSH가 사용할 수 없는 주소를 알렸습니다. SSH 연결을 통해 Mosh 주소를 확인합니다."
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Ekstern SSH oppga en ubrukelig adresse; Mosh-adressen løses via SSH-forbindelsen."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Zdalny SSH ogłosił nieużyteczny adres; adres Mosh jest ustalany przez połączenie SSH."
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] O SSH remoto anunciou um endereço inutilizável; resolvendo o endereço do Mosh pela conexão SSH."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Удалённый SSH сообщил непригодный адрес; адрес Mosh определяется через SSH-подключение."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] SSH ระยะไกลแจ้งที่อยู่ที่ใช้ไม่ได้ จึงค้นหาที่อยู่ Mosh ผ่านการเชื่อมต่อ SSH"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Uzak SSH kullanılamayan bir adres bildirdi; Mosh adresi SSH bağlantısı üzerinden çözümleniyor."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] Віддалений SSH повідомив непридатну адресу; адреса Mosh визначається через SSH-з’єднання."
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 远程 SSH 宣布的地址不可用;将通过 SSH 连接解析 Mosh 地址。"
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "[cmux] 遠端 SSH 回報的位址無法使用;將透過 SSH 連線解析 Mosh 位址。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli.ssh.mosh.remoteMissing": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
||||
@@ -307,12 +307,17 @@ extension SessionRemoteWorkspaceSnapshot {
|
||||
let sshInvocation = terminalArguments
|
||||
.map(Self.shellQuote)
|
||||
.joined(separator: " ")
|
||||
let remoteCommandTemplate = [
|
||||
let remoteCommandScript = [
|
||||
restoredSSHTerminalConnectedShellScript(
|
||||
remoteRelayPort: remoteRelayPort
|
||||
),
|
||||
staging.remoteExecutionShellScript,
|
||||
].joined(separator: "\n")
|
||||
// OpenSSH gives the command after the destination to the account's
|
||||
// configured login shell. Keep fish/csh/nushell from parsing the
|
||||
// POSIX lifecycle/relay script by making /bin/sh the command's
|
||||
// outermost interpreter explicitly.
|
||||
let remoteCommandTemplate = "/bin/sh -c \(Self.shellQuote(remoteCommandScript))"
|
||||
let script = [
|
||||
"cmux_restore_fail() { \(failureScript); }",
|
||||
"cmux_restore_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"",
|
||||
@@ -431,6 +436,14 @@ extension SessionRemoteWorkspaceSnapshot {
|
||||
remoteMoshProbeFailedMessage: String(
|
||||
localized: "cli.ssh.mosh.probeFailed",
|
||||
defaultValue: "[cmux] Could not verify remote Mosh support; continuing over SSH."
|
||||
),
|
||||
remoteBootstrapInstallFailedMessage: String(
|
||||
localized: "cli.ssh.mosh.bootstrapInstallFailed",
|
||||
defaultValue: "[cmux] Remote bootstrap install failed; continuing over SSH."
|
||||
),
|
||||
remoteMoshAddressFallbackMessage: String(
|
||||
localized: "cli.ssh.mosh.addressFallback",
|
||||
defaultValue: "[cmux] Remote SSH advertised an unusable address; resolving the Mosh address through the SSH connection."
|
||||
)
|
||||
).command()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ struct SessionRemoteWorkspaceMoshRestoreTests {
|
||||
localMoshMissingMessage: "local Mosh missing",
|
||||
localMoshUnsupportedMessage: "local Mosh unsupported",
|
||||
remoteMoshMissingMessage: "remote Mosh missing",
|
||||
remoteMoshProbeFailedMessage: "remote Mosh probe failed"
|
||||
remoteMoshProbeFailedMessage: "remote Mosh probe failed",
|
||||
remoteBootstrapInstallFailedMessage: "remote bootstrap install failed",
|
||||
remoteMoshAddressFallbackMessage: "remote Mosh address fallback"
|
||||
).command()
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/sh")
|
||||
@@ -84,7 +86,7 @@ struct SessionRemoteWorkspaceMoshRestoreTests {
|
||||
let command = try #require(configuration.terminalStartupCommand)
|
||||
|
||||
#expect(configuration.terminalTransport == .mosh)
|
||||
#expect(command.contains("--experimental-remote-ip=remote"), "\(command)")
|
||||
#expect(command.contains("cmux_mosh_remote_ip_mode"), "\(command)")
|
||||
#expect(command.contains("[email protected]"), "\(command)")
|
||||
#expect(command.contains("2222"), "\(command)")
|
||||
#expect(command.contains("ProxyJump=bastion"), "\(command)")
|
||||
|
||||
@@ -35,7 +35,7 @@ This is a **living implementation spec** (also called an **execution spec**): a
|
||||
- `DONE` `cmux mosh <destination>` is a command-level alias for the same first-class remote-workspace path, with Mosh selected by default.
|
||||
- `DONE` `cmux mosh-tmux <destination> [--session <name>]` creates or attaches a terminal-hosted tmux session over Mosh while preserving remote metadata, daemon/control, relay, proxy/egress, upload, and reconnect behavior. The typed shell-or-tmux terminal profile persists across workspace reconnect and app session restore.
|
||||
- `DONE` terminal transport is separate from management transport. Mosh carries only the interactive PTY; SSH remains responsible for `cmuxd-remote` upload/bootstrap, daemon RPC, the reverse CLI relay, proxy/egress traffic, file uploads, capability probes, and reconnect controls.
|
||||
- `DONE` cmux requires a local Mosh client with `--experimental-remote-ip=remote` support (Mosh 1.4+), then checks for remote `mosh-server`. A missing/incompatible client, missing server, or failed capability probe produces an explicit message and falls back to the existing SSH terminal command.
|
||||
- `DONE` cmux requires a local Mosh client with `--experimental-remote-ip` support (Mosh 1.4+), stages the cmux bootstrap, and probes for an already-installed remote `mosh-server` through an explicit POSIX `/bin/sh` management lane (cmux does not install Mosh itself). When `SSH_CONNECTION` is empty or unusable, the launcher selects Mosh's SSH-proxy address-resolution mode and explains the fallback. A missing/incompatible client, bootstrap-install failure, missing server, or failed capability probe produces a stage-specific message and falls back to the existing SSH terminal command.
|
||||
- `DEFERRED` Native `ssh-tmux`-style mirroring over Mosh: tmux control mode requires a lossless byte stream, while Mosh exposes synchronized terminal screen state. `mosh-tmux` therefore provides a real roaming terminal attach, not a mislabeled native mirror. Also deferred: running the daemon/control lane over Mosh and automatic recovery from blocked UDP after a successful Mosh capability probe.
|
||||
|
||||
### 3.2 Bootstrap + Daemon
|
||||
|
||||
Reference in New Issue
Block a user