Share no ssh connection when the socket path would be too long

macOS accepts socket paths of at most 103 characters. Count the folder,
the file name, the 40-character hash and the temporary end ssh adds; when
the total is over 103, leave the connection-sharing options out. Find,
make and check the socket folder once per run instead of for every
command.
This commit is contained in:
2026-10-15 15:31:08 +02:00
parent 25308f5f0f
commit bd7c129116
2 changed files with 73 additions and 5 deletions
@@ -69,7 +69,12 @@ A path on the server that is `~` or starts with `~/` is handed to the server's s
`gites` folder in the user's cache folder, made so that only the user can open it;
never in `/tmp`. If neither folder can be found or made, these three options are
left out and every command opens a new connection. `%C` is a hash ssh makes from
the host, port and user, so the path stays short.
the host, port and user, 40 characters long. macOS accepts a socket path of at most
103 characters (Linux 107), and while ssh sets up a shared connection it adds a
temporary end of up to 18 characters to the file name. So gites counts the folder's
length + 1 + 6 (`gites-`) + 40 + 18; if that is more than 103 (one limit on every
system), these three options are left out too. The folder is found, made and checked
once when gites first runs ssh, not for every command.
- On Windows, ssh cannot share connections, so these three options are left out and
every command opens a new connection.
- Quoting: every word of the git command is put in single quotes, and a `'` inside a
+67 -4
View File
@@ -75,6 +75,33 @@ fn socket_dir(runtime: Option<PathBuf>, cache: Option<PathBuf>) -> Option<PathBu
Some(dir)
}
/// The start of every socket's file name; ssh puts `%C`, a hash of the connection, after it.
#[cfg(not(windows))]
const SOCKET_PREFIX: &str = "gites-";
/// The longest socket path macOS accepts (Linux accepts 107). One limit for every system
/// keeps one rule.
#[cfg(not(windows))]
const MAX_SOCKET_PATH: usize = 103;
/// True when sockets in `dir` stay short enough for the system. ssh puts in place of `%C` a
/// hash of 40 characters, and while it sets up a shared connection it adds a temporary end of
/// up to 18 characters to the file name.
#[cfg(not(windows))]
fn socket_path_fits(dir: &std::path::Path) -> bool {
const HASH: usize = 40;
const TEMPORARY_END: usize = 18;
// The 1 is the `/` between the folder and the file name.
dir.as_os_str().len() + 1 + SOCKET_PREFIX.len() + HASH + TEMPORARY_END <= MAX_SOCKET_PATH
}
/// The path ssh is given for the socket of shared connections, or `None` to share none: when
/// there is no folder for sockets, or the path would be too long.
#[cfg(not(windows))]
fn control_path(dir: Option<PathBuf>) -> Option<PathBuf> {
dir.filter(|d| socket_path_fits(d)).map(|d| d.join(format!("{SOCKET_PREFIX}%C")))
}
/// Runs git in the folder `loc` names: here, or on another machine through ssh.
pub fn git(loc: &Location, args: &[&str]) -> Result<String, String> {
// Tests swap in a script that acts like ssh.
@@ -104,11 +131,17 @@ pub fn git_via(ssh: &[OsString], loc: &Location, args: &[&str]) -> Result<String
cmd.args(["-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2"]);
// Later commands to the same server reuse one connection for a while. Windows ssh
// does not support this.
// The folder is found, made and checked once per run, not for every command.
#[cfg(not(windows))]
if let Some(dir) = socket_dir(dirs::runtime_dir(), dirs::cache_dir()) {
cmd.args(["-o", "ControlMaster=auto", "-o"]);
cmd.arg(format!("ControlPath={}", dir.join("gites-%C").display()));
cmd.args(["-o", "ControlPersist=10m"]);
{
static CONTROL_PATH: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
let path = CONTROL_PATH
.get_or_init(|| control_path(socket_dir(dirs::runtime_dir(), dirs::cache_dir())));
if let Some(path) = path {
cmd.args(["-o", "ControlMaster=auto", "-o"]);
cmd.arg(format!("ControlPath={}", path.display()));
cmd.args(["-o", "ControlPersist=10m"]);
}
}
// `--` keeps a target from being read as an option.
cmd.arg("--").arg(target).arg(remote_command(path, args));
@@ -399,6 +432,36 @@ mod tests {
let _ = std::fs::remove_dir_all(&base);
}
#[cfg(not(windows))]
#[test]
fn shares_connections_through_a_socket_in_a_short_folder() {
assert!(socket_path_fits(&PathBuf::from("/run/user/1000")));
assert_eq!(
control_path(Some(PathBuf::from("/run/user/1000"))),
Some(PathBuf::from("/run/user/1000/gites-%C"))
);
}
#[cfg(not(windows))]
#[test]
fn shares_no_connection_when_the_socket_path_would_be_too_long() {
// The longest folder that fits: the rest of the path takes 1 + 6 + 40 + 18 characters.
let longest = 103 - 1 - SOCKET_PREFIX.len() - 40 - 18;
let too_long = PathBuf::from(format!("/{}", "a".repeat(longest)));
assert!(!socket_path_fits(&too_long));
assert_eq!(control_path(Some(too_long)), None);
assert_eq!(control_path(None), None);
}
#[cfg(not(windows))]
#[test]
fn shares_connections_when_the_socket_path_is_exactly_at_the_limit() {
let longest = 103 - 1 - SOCKET_PREFIX.len() - 40 - 18;
let at_limit = PathBuf::from(format!("/{}", "a".repeat(longest - 1)));
assert_eq!(at_limit.as_os_str().len(), longest);
assert!(socket_path_fits(&at_limit));
}
#[test]
fn reports_a_missing_ssh_program() {
let loc = Location::Ssh { target: "myserver".into(), path: "/x".into() };