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:
@@ -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
@@ -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() };
|
||||
|
||||
Reference in New Issue
Block a user