feat(host): an AppImage's host learns its image and restarts onto a replaced one (P11a step 3 gap)
An AppImage runs the program from a mount that goes away with the image it came from, so a host held open past an update kept running the old mount and never noticed the replaced image. The host now reads APPIMAGE (added to its environment allow-list only when it names an absolute file; it is a path the host reads about itself and never tells an agent, so I-2 holds), runs and restarts from that file, and tells a replaced program by its file's inode, size and last write. Test: a_replaced_appimage_restarts_the_books (the programs run from one file while APPIMAGE names another, as in a mount). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
5ecc01b682
commit
562b893c64
3 files changed
+167
-37
No files matched your search
@@ -76,6 +76,10 @@ fn base_program() -> &'static Path {
|
|||||||
struct Home {
|
struct Home {
|
||||||
dir: tempfile::TempDir,
|
dir: tempfile::TempDir,
|
||||||
exe: PathBuf,
|
exe: PathBuf,
|
||||||
|
/// A pretend AppImage (P11a step 4): the programs run from `exe`, as
|
||||||
|
/// from an image's mount, and `APPIMAGE` names this other file, the
|
||||||
|
/// one an update replaces.
|
||||||
|
image: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Home {
|
impl Home {
|
||||||
@@ -95,7 +99,22 @@ impl Home {
|
|||||||
std::fs::create_dir(&bin).unwrap();
|
std::fs::create_dir(&bin).unwrap();
|
||||||
let exe = bin.join("wanderbooks");
|
let exe = bin.join("wanderbooks");
|
||||||
std::fs::copy(base_program(), &exe).unwrap();
|
std::fs::copy(base_program(), &exe).unwrap();
|
||||||
let home = Self { dir, exe };
|
let home = Self {
|
||||||
|
dir,
|
||||||
|
exe,
|
||||||
|
image: None,
|
||||||
|
};
|
||||||
|
home.runnable();
|
||||||
|
home
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A home whose program is an AppImage: the same program twice, one
|
||||||
|
/// copy to run (the mount's) and one the image file `APPIMAGE` names.
|
||||||
|
fn new_image(tag: &str) -> Self {
|
||||||
|
let mut home = Self::new(tag);
|
||||||
|
let image = home.dir.path().join("Wanderbooks.AppImage");
|
||||||
|
std::fs::copy(base_program(), &image).unwrap();
|
||||||
|
home.image = Some(image);
|
||||||
home.runnable();
|
home.runnable();
|
||||||
home
|
home
|
||||||
}
|
}
|
||||||
@@ -103,25 +122,35 @@ impl Home {
|
|||||||
/// Wait until the copy can be run (a child another test thread forked
|
/// Wait until the copy can be run (a child another test thread forked
|
||||||
/// while it was written may hold it open for a moment).
|
/// while it was written may hold it open for a moment).
|
||||||
fn runnable(&self) {
|
fn runnable(&self) {
|
||||||
let deadline = Instant::now() + Duration::from_secs(10);
|
for exe in std::iter::once(&self.exe).chain(self.image.as_ref()) {
|
||||||
loop {
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
match Command::new(&self.exe)
|
loop {
|
||||||
.arg("--help")
|
match Command::new(exe)
|
||||||
.stdin(Stdio::null())
|
.arg("--help")
|
||||||
.stdout(Stdio::null())
|
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
|
||||||
.stderr(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.status()
|
.stdout(Stdio::null())
|
||||||
{
|
.stderr(Stdio::null())
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
|
.status()
|
||||||
assert!(Instant::now() < deadline, "the copy stayed busy");
|
{
|
||||||
std::thread::sleep(Duration::from_millis(20));
|
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
|
||||||
|
assert!(Instant::now() < deadline, "the copy stayed busy");
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
Err(e) => panic!("the copy can't be run: {e}"),
|
||||||
|
Ok(_) => break,
|
||||||
}
|
}
|
||||||
Err(e) => panic!("the copy can't be run: {e}"),
|
|
||||||
Ok(_) => return,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The environment of the pretend AppImage, if this is one.
|
||||||
|
fn image_env(&self, c: &mut Command) {
|
||||||
|
if let Some(image) = &self.image {
|
||||||
|
c.env("APPIMAGE", image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn root(&self) -> PathBuf {
|
fn root(&self) -> PathBuf {
|
||||||
self.dir.path().join(".local/share/wanderbooks")
|
self.dir.path().join(".local/share/wanderbooks")
|
||||||
}
|
}
|
||||||
@@ -139,11 +168,13 @@ impl Home {
|
|||||||
.env_clear()
|
.env_clear()
|
||||||
.env("PATH", "/usr/bin:/bin")
|
.env("PATH", "/usr/bin:/bin")
|
||||||
.env("HOME", self.dir.path());
|
.env("HOME", self.dir.path());
|
||||||
|
self.image_env(&mut c);
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The program that updates replace: the image, if there is one.
|
||||||
fn program(&self) -> Vec<u8> {
|
fn program(&self) -> Vec<u8> {
|
||||||
std::fs::read(&self.exe).unwrap()
|
std::fs::read(self.image.as_ref().unwrap_or(&self.exe)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(&self, id: &str) {
|
fn create(&self, id: &str) {
|
||||||
@@ -180,6 +211,7 @@ impl Home {
|
|||||||
.env("PATH", "/usr/bin:/bin")
|
.env("PATH", "/usr/bin:/bin")
|
||||||
.env("HOME", self.dir.path())
|
.env("HOME", self.dir.path())
|
||||||
.stdin(Stdio::null());
|
.stdin(Stdio::null());
|
||||||
|
self.image_env(&mut c);
|
||||||
c.output().unwrap()
|
c.output().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,7 +687,11 @@ impl Drop for Agent {
|
|||||||
/// host and server run the new file afterwards, and the head is the one
|
/// host and server run the new file afterwards, and the head is the one
|
||||||
/// the writes made.
|
/// the writes made.
|
||||||
fn update_under_writes(tag: &str) -> (Home, Agent, Vec<String>) {
|
fn update_under_writes(tag: &str) -> (Home, Agent, Vec<String>) {
|
||||||
let home = Home::new(tag);
|
update_under_writes_in(Home::new(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_under_writes_in(home: Home) -> (Home, Agent, Vec<String>) {
|
||||||
|
let tag = "under-writes";
|
||||||
home.create("acme");
|
home.create("acme");
|
||||||
let release = Release::new();
|
let release = Release::new();
|
||||||
let next = newer(&home, tag);
|
let next = newer(&home, tag);
|
||||||
@@ -673,18 +709,21 @@ fn update_under_writes(tag: &str) -> (Home, Agent, Vec<String>) {
|
|||||||
release.dir.path().join("manifest.json").display()
|
release.dir.path().join("manifest.json").display()
|
||||||
);
|
);
|
||||||
let key = release.dir.path().join("release.pub");
|
let key = release.dir.path().join("release.pub");
|
||||||
|
let image = home.image.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let _keep = &release;
|
let _keep = &release;
|
||||||
Command::new(exe)
|
let mut c = Command::new(exe);
|
||||||
.args(["update", "--root"])
|
c.args(["update", "--root"])
|
||||||
.arg(root)
|
.arg(root)
|
||||||
.args(["--feed", &feed, "--trusted-key"])
|
.args(["--feed", &feed, "--trusted-key"])
|
||||||
.arg(key)
|
.arg(key)
|
||||||
.env_clear()
|
.env_clear()
|
||||||
.env("PATH", "/usr/bin:/bin")
|
.env("PATH", "/usr/bin:/bin")
|
||||||
.env("HOME", dir)
|
.env("HOME", dir);
|
||||||
.output()
|
if let Some(image) = image {
|
||||||
.unwrap()
|
c.env("APPIMAGE", image);
|
||||||
|
}
|
||||||
|
c.output().unwrap()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let mut written = vec!["4000".to_string()];
|
let mut written = vec!["4000".to_string()];
|
||||||
@@ -742,6 +781,39 @@ fn bridge_survives_update() {
|
|||||||
assert!(in_books >= written.len(), "{in_books} < {}", written.len());
|
assert!(in_books >= written.len(), "{in_books} < {}", written.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_replaced_appimage_restarts_the_books() {
|
||||||
|
// The programs run from one file (an image's mount) while `APPIMAGE`
|
||||||
|
// names another (the image). Replacing the image alone must restart
|
||||||
|
// the host onto it, with the session and every write intact.
|
||||||
|
let home = Home::new_image("appimage");
|
||||||
|
let image = home.image.clone().unwrap();
|
||||||
|
home.create("acme");
|
||||||
|
let release = Release::new();
|
||||||
|
let next = newer(&home, "appimage");
|
||||||
|
release.publish("9.0.0", &next, |_| {});
|
||||||
|
let mut agent = Agent::start(&home, "acme");
|
||||||
|
agent.add_account("4000").unwrap();
|
||||||
|
let host_before = wait_host(&home);
|
||||||
|
let out = home.run("update", Some(&release));
|
||||||
|
assert!(out.status.success(), "{}", text(&out));
|
||||||
|
assert_eq!(std::fs::read(&image).unwrap(), next);
|
||||||
|
// The mount's copy is untouched; only the image changed.
|
||||||
|
assert_ne!(std::fs::read(&home.exe).unwrap(), next);
|
||||||
|
assert!(
|
||||||
|
wait_for(60, || {
|
||||||
|
home.pids("host").len() == 1 && home.pids("host")[0] != host_before
|
||||||
|
}),
|
||||||
|
"the host never restarted onto the replaced image"
|
||||||
|
);
|
||||||
|
let host = wait_host(&home);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_link(format!("/proc/{host}/exe")).unwrap(),
|
||||||
|
image
|
||||||
|
);
|
||||||
|
assert!(agent.add_account("4001").is_ok(), "the session carries on");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_waits_for_a_running_call() {
|
fn update_waits_for_a_running_call() {
|
||||||
// No call is cut off or made twice: each account written across the
|
// No call is cut off or made twice: each account written across the
|
||||||
|
|||||||
@@ -196,28 +196,46 @@ fn children_executable() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A program file's identity: device and inode, size and last write. A
|
||||||
|
/// replaced file differs in the inode, and a rewritten one in the rest.
|
||||||
|
type Identity = (u64, u64, u64, i64, i64);
|
||||||
|
|
||||||
|
fn identity_from(meta: &std::fs::Metadata) -> Identity {
|
||||||
|
(
|
||||||
|
meta.dev(),
|
||||||
|
meta.ino(),
|
||||||
|
meta.len(),
|
||||||
|
meta.mtime(),
|
||||||
|
meta.mtime_nsec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// The running executable's file identity, and its file's now: they
|
/// The running executable's file identity, and its file's now: they
|
||||||
/// differ once the file was replaced (INVARIANT 7).
|
/// differ once the file was replaced (INVARIANT 7). An AppImage is its
|
||||||
fn running_identity() -> Option<(u64, u64)> {
|
/// image file (`APPIMAGE`), not the program inside the mount.
|
||||||
|
fn running_identity() -> Option<Identity> {
|
||||||
|
if let Some(image) = crate::start::own_appimage(std::env::var_os("APPIMAGE").as_deref()) {
|
||||||
|
return Some(identity_from(&std::fs::metadata(image).ok()?));
|
||||||
|
}
|
||||||
let running = if cfg!(target_os = "linux") {
|
let running = if cfg!(target_os = "linux") {
|
||||||
std::fs::metadata("/proc/self/exe").ok()?
|
std::fs::metadata("/proc/self/exe").ok()?
|
||||||
} else {
|
} else {
|
||||||
std::fs::metadata(std::env::current_exe().ok()?).ok()?
|
std::fs::metadata(std::env::current_exe().ok()?).ok()?
|
||||||
};
|
};
|
||||||
Some((running.dev(), running.ino()))
|
Some(identity_from(&running))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The identity of the file at this executable's path, if it is a
|
/// The identity of the file at this executable's path, if it is a
|
||||||
/// program (a file half-written or gone doesn't count yet).
|
/// program (a file half-written or gone doesn't count yet).
|
||||||
fn file_identity() -> Option<(u64, u64)> {
|
fn file_identity() -> Option<Identity> {
|
||||||
identity_of(&crate::start::this_executable().ok()?)
|
identity_of(&crate::start::this_executable().ok()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The identity (device, inode) of the program at `path`: a file someone
|
/// The identity of the program at `path`: a file someone may run; else
|
||||||
/// may run; else None.
|
/// None.
|
||||||
fn identity_of(path: &Path) -> Option<(u64, u64)> {
|
fn identity_of(path: &Path) -> Option<Identity> {
|
||||||
let meta = std::fs::metadata(path).ok()?;
|
let meta = std::fs::metadata(path).ok()?;
|
||||||
(meta.is_file() && meta.permissions().mode() & 0o111 != 0).then(|| (meta.dev(), meta.ino()))
|
(meta.is_file() && meta.permissions().mode() & 0o111 != 0).then(|| identity_from(&meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait at most `wait` for a host to answer on the lease socket: whether
|
/// Wait at most `wait` for a host to answer on the lease socket: whether
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
//! it names a socket this account owns (the owner's ssh agent, so the
|
//! it names a socket this account owns (the owner's ssh agent, so the
|
||||||
//! server's backups reach a private git repository, D-30). Nothing
|
//! server's backups reach a private git repository, D-30). Nothing
|
||||||
//! else passes (a scope's own identifiers, such as INVOCATION_ID, are
|
//! else passes (a scope's own identifiers, such as INVOCATION_ID, are
|
||||||
//! systemd's).
|
//! systemd's). One more: APPIMAGE, only when it names an absolute
|
||||||
|
//! file, so the host knows its own AppImage (below).
|
||||||
//! 2. Its standard input and output are null, its errors go to
|
//! 2. Its standard input and output are null, its errors go to
|
||||||
//! run/host.log (0600); it starts a new session with umask 077. Every
|
//! run/host.log (0600); it starts a new session with umask 077. Every
|
||||||
//! other descriptor is closed (std opens them close-on-exec, and the
|
//! other descriptor is closed (std opens them close-on-exec, and the
|
||||||
@@ -23,7 +24,11 @@
|
|||||||
//! scope couldn't be made (systemd-run failed without the host ever
|
//! scope couldn't be made (systemd-run failed without the host ever
|
||||||
//! writing its first line), or there is no such scope, directly. A
|
//! writing its first line), or there is no such scope, directly. A
|
||||||
//! host that ran and stopped is never started a second time.
|
//! host that ran and stopped is never started a second time.
|
||||||
//! 4. It runs this executable's file as it is on disk now.
|
//! 4. It runs this executable's file as it is on disk now; for an
|
||||||
|
//! AppImage that is the image file (APPIMAGE), not the program inside
|
||||||
|
//! its mount, which goes away with the image it came from. The host
|
||||||
|
//! reads that path about itself and never tells it to an agent (I-2),
|
||||||
|
//! so a replaced image restarts the books like any replaced program.
|
||||||
//! 5. The opener doesn't wait for it, and reaps it when it ends.
|
//! 5. The opener doesn't wait for it, and reaps it when it ends.
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
@@ -49,6 +54,9 @@ const LOG_LIMIT: u64 = 8 * 1024 * 1024;
|
|||||||
/// replaced file reads as "… (deleted)"; the path, not the old file, is
|
/// replaced file reads as "… (deleted)"; the path, not the old file, is
|
||||||
/// what a new host runs.
|
/// what a new host runs.
|
||||||
pub fn this_executable() -> Result<PathBuf, String> {
|
pub fn this_executable() -> Result<PathBuf, String> {
|
||||||
|
if let Some(image) = own_appimage(std::env::var_os("APPIMAGE").as_deref()) {
|
||||||
|
return Ok(PathBuf::from(image));
|
||||||
|
}
|
||||||
let exe = std::env::current_exe().map_err(|e| format!("can't find this program: {e}"))?;
|
let exe = std::env::current_exe().map_err(|e| format!("can't find this program: {e}"))?;
|
||||||
let text = exe.to_string_lossy();
|
let text = exe.to_string_lossy();
|
||||||
if let Some(path) = text.strip_suffix(" (deleted)")
|
if let Some(path) = text.strip_suffix(" (deleted)")
|
||||||
@@ -59,6 +67,13 @@ pub fn this_executable() -> Result<PathBuf, String> {
|
|||||||
Ok(exe)
|
Ok(exe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The AppImage this process runs from: `APPIMAGE` when it is an
|
||||||
|
/// absolute path to a file.
|
||||||
|
pub fn own_appimage(value: Option<&std::ffi::OsStr>) -> Option<std::ffi::OsString> {
|
||||||
|
let path = Path::new(value?);
|
||||||
|
(path.is_absolute() && path.is_file()).then(|| path.as_os_str().to_os_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// This account's user ID.
|
/// This account's user ID.
|
||||||
fn own_uid() -> u32 {
|
fn own_uid() -> u32 {
|
||||||
// SAFETY: geteuid has no preconditions and can't fail.
|
// SAFETY: geteuid has no preconditions and can't fail.
|
||||||
@@ -128,7 +143,11 @@ fn own_agent(named: Option<&std::ffi::OsStr>) -> Option<std::ffi::OsString> {
|
|||||||
|
|
||||||
/// The host's environment (INVARIANT 1), given the opener's
|
/// The host's environment (INVARIANT 1), given the opener's
|
||||||
/// `SSH_AUTH_SOCK`.
|
/// `SSH_AUTH_SOCK`.
|
||||||
fn host_env(home: &Path, ssh_agent: Option<&std::ffi::OsStr>) -> Vec<(String, std::ffi::OsString)> {
|
fn host_env(
|
||||||
|
home: &Path,
|
||||||
|
ssh_agent: Option<&std::ffi::OsStr>,
|
||||||
|
appimage: Option<&std::ffi::OsStr>,
|
||||||
|
) -> Vec<(String, std::ffi::OsString)> {
|
||||||
let mut env = vec![
|
let mut env = vec![
|
||||||
("PATH".to_string(), PATH.into()),
|
("PATH".to_string(), PATH.into()),
|
||||||
("HOME".to_string(), home.as_os_str().to_os_string()),
|
("HOME".to_string(), home.as_os_str().to_os_string()),
|
||||||
@@ -142,6 +161,9 @@ fn host_env(home: &Path, ssh_agent: Option<&std::ffi::OsStr>) -> Vec<(String, st
|
|||||||
if let Some(agent) = own_agent(ssh_agent) {
|
if let Some(agent) = own_agent(ssh_agent) {
|
||||||
env.push(("SSH_AUTH_SOCK".into(), agent));
|
env.push(("SSH_AUTH_SOCK".into(), agent));
|
||||||
}
|
}
|
||||||
|
if let Some(image) = own_appimage(appimage) {
|
||||||
|
env.push(("APPIMAGE".into(), image));
|
||||||
|
}
|
||||||
env
|
env
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +249,7 @@ pub fn start_host(
|
|||||||
.envs(host_env(
|
.envs(host_env(
|
||||||
&home,
|
&home,
|
||||||
std::env::var_os("SSH_AUTH_SOCK").as_deref(),
|
std::env::var_os("SSH_AUTH_SOCK").as_deref(),
|
||||||
|
std::env::var_os("APPIMAGE").as_deref(),
|
||||||
))
|
))
|
||||||
.current_dir("/")
|
.current_dir("/")
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
@@ -385,7 +408,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_host_environment_carries_nothing_else() {
|
fn the_host_environment_carries_nothing_else() {
|
||||||
let env = host_env(Path::new("/home/someone"), None);
|
let env = host_env(Path::new("/home/someone"), None, None);
|
||||||
let names: Vec<&str> = env.iter().map(|(n, _)| n.as_str()).collect();
|
let names: Vec<&str> = env.iter().map(|(n, _)| n.as_str()).collect();
|
||||||
assert_eq!(&names[..2], ["PATH", "HOME"]);
|
assert_eq!(&names[..2], ["PATH", "HOME"]);
|
||||||
for n in &names[2..] {
|
for n in &names[2..] {
|
||||||
@@ -393,7 +416,8 @@ mod tests {
|
|||||||
[
|
[
|
||||||
"XDG_RUNTIME_DIR",
|
"XDG_RUNTIME_DIR",
|
||||||
"DBUS_SESSION_BUS_ADDRESS",
|
"DBUS_SESSION_BUS_ADDRESS",
|
||||||
"SSH_AUTH_SOCK"
|
"SSH_AUTH_SOCK",
|
||||||
|
"APPIMAGE"
|
||||||
]
|
]
|
||||||
.contains(n),
|
.contains(n),
|
||||||
"{n}"
|
"{n}"
|
||||||
@@ -403,6 +427,22 @@ mod tests {
|
|||||||
assert_eq!(env[1].1, "/home/someone");
|
assert_eq!(env[1].1, "/home/someone");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_an_absolute_file_passes_as_the_appimage() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let image = dir.path().join("W.AppImage");
|
||||||
|
std::fs::write(&image, b"x").unwrap();
|
||||||
|
let home = Path::new("/home/someone");
|
||||||
|
let has = |v: Option<&std::ffi::OsStr>| {
|
||||||
|
host_env(home, None, v).iter().any(|(n, _)| n == "APPIMAGE")
|
||||||
|
};
|
||||||
|
assert!(has(Some(image.as_os_str())));
|
||||||
|
assert!(!has(None));
|
||||||
|
assert!(!has(Some("W.AppImage".as_ref())), "relative");
|
||||||
|
assert!(!has(Some(dir.path().as_os_str())), "a folder");
|
||||||
|
assert!(!has(Some(dir.path().join("gone").as_os_str())));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_an_own_ssh_agent_socket_passes() {
|
fn only_an_own_ssh_agent_socket_passes() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -413,7 +453,7 @@ mod tests {
|
|||||||
Some(sock.clone().into_os_string())
|
Some(sock.clone().into_os_string())
|
||||||
);
|
);
|
||||||
// It reaches the host's environment, once.
|
// It reaches the host's environment, once.
|
||||||
let env = host_env(Path::new("/home/someone"), Some(sock.as_os_str()));
|
let env = host_env(Path::new("/home/someone"), Some(sock.as_os_str()), None);
|
||||||
let agents: Vec<_> = env.iter().filter(|(n, _)| n == "SSH_AUTH_SOCK").collect();
|
let agents: Vec<_> = env.iter().filter(|(n, _)| n == "SSH_AUTH_SOCK").collect();
|
||||||
assert_eq!(agents.len(), 1);
|
assert_eq!(agents.len(), 1);
|
||||||
assert_eq!(agents[0].1, sock.as_os_str());
|
assert_eq!(agents[0].1, sock.as_os_str());
|
||||||
@@ -422,7 +462,7 @@ mod tests {
|
|||||||
let link = dir.path().join("link.sock");
|
let link = dir.path().join("link.sock");
|
||||||
std::os::unix::fs::symlink(&sock, &link).unwrap();
|
std::os::unix::fs::symlink(&sock, &link).unwrap();
|
||||||
assert_eq!(own_agent(Some(link.as_os_str())), None);
|
assert_eq!(own_agent(Some(link.as_os_str())), None);
|
||||||
let env = host_env(Path::new("/home/someone"), Some(link.as_os_str()));
|
let env = host_env(Path::new("/home/someone"), Some(link.as_os_str()), None);
|
||||||
assert!(env.iter().all(|(n, _)| n != "SSH_AUTH_SOCK"));
|
assert!(env.iter().all(|(n, _)| n != "SSH_AUTH_SOCK"));
|
||||||
// Named relative to here: not taken, even where it leads to one.
|
// Named relative to here: not taken, even where it leads to one.
|
||||||
let here = std::env::current_dir().unwrap();
|
let here = std::env::current_dir().unwrap();
|
||||||
|
|||||||
Reference in new issue
Block a user