| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
pub const PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; |
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
pub fn resolve_program(program: &str, path: &str) -> Option<String> { |
| 12 |
use std::os::unix::fs::PermissionsExt; |
| 13 |
if program.is_empty() { |
| 14 |
return None; |
| 15 |
} |
| 16 |
if program.contains('/') { |
| 17 |
return Some(program.to_string()); |
| 18 |
} |
| 19 |
path.split(':') |
| 20 |
.filter(|d| !d.is_empty()) |
| 21 |
.map(|d| format!("{d}/{program}")) |
| 22 |
.find(|p| std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)) |
| 23 |
} |
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
pub fn job_env(image: Option<&str>, job: &[(String, String)]) -> std::collections::BTreeMap<String, String> { |
| 30 |
let mut env = std::collections::BTreeMap::new(); |
| 31 |
env.insert("PATH".to_string(), PATH.to_string()); |
| 32 |
env.insert("HOME".to_string(), "/tmp".to_string()); |
| 33 |
for line in image.unwrap_or("").lines() { |
| 34 |
if let Some((k, v)) = line.split_once('=') { |
| 35 |
if !k.is_empty() { |
| 36 |
env.insert(k.to_string(), v.to_string()); |
| 37 |
} |
| 38 |
} |
| 39 |
} |
| 40 |
for (k, v) in job { |
| 41 |
env.insert(k.clone(), v.clone()); |
| 42 |
} |
| 43 |
env |
| 44 |
} |
| 45 |
|
| 46 |
|
| 47 |
|
| 48 |
|
| 49 |
pub fn dev_links(dev: &str) -> Result<(), String> { |
| 50 |
for (name, target) in DEV_LINKS { |
| 51 |
let link = format!("{dev}/{name}"); |
| 52 |
if std::fs::symlink_metadata(&link).is_err() { |
| 53 |
std::os::unix::fs::symlink(target, &link).map_err(|e| format!("link {link}: {e}"))?; |
| 54 |
} |
| 55 |
} |
| 56 |
Ok(()) |
| 57 |
} |
| 58 |
|
| 59 |
const DEV_LINKS: [(&str, &str); 4] = |
| 60 |
[("fd", "/proc/self/fd"), ("stdin", "/proc/self/fd/0"), ("stdout", "/proc/self/fd/1"), ("stderr", "/proc/self/fd/2")]; |
| 61 |
|
| 62 |
|
| 63 |
pub fn loopback_up() -> Result<(), String> { |
| 64 |
let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) }; |
| 65 |
if fd < 0 { |
| 66 |
return Err(format!("socket for lo: {}", std::io::Error::last_os_error())); |
| 67 |
} |
| 68 |
let mut req: libc::ifreq = unsafe { std::mem::zeroed() }; |
| 69 |
for (i, b) in b"lo".iter().enumerate() { |
| 70 |
req.ifr_name[i] = *b as libc::c_char; |
| 71 |
} |
| 72 |
let ok = unsafe { |
| 73 |
libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut req) == 0 && { |
| 74 |
req.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short; |
| 75 |
libc::ioctl(fd, libc::SIOCSIFFLAGS, &req) == 0 |
| 76 |
} |
| 77 |
}; |
| 78 |
let err = std::io::Error::last_os_error(); |
| 79 |
unsafe { libc::close(fd) }; |
| 80 |
if !ok { |
| 81 |
return Err(format!("bring up lo: {err}")); |
| 82 |
} |
| 83 |
Ok(()) |
| 84 |
} |
| 85 |
|
| 86 |
#[cfg(test)] |
| 87 |
mod tests { |
| 88 |
use super::{dev_links, job_env, resolve_program}; |
| 89 |
|
| 90 |
#[test] |
| 91 |
fn the_image_environment_sits_under_the_jobs() { |
| 92 |
let image = "PATH=/opt/llvm-mos/bin:/usr/bin:/bin\nX16EMU=/opt/x16emu\nnot a line\n"; |
| 93 |
let env = job_env(Some(image), &[("X16EMU".into(), "/job".into()), ("LANG".into(), "C".into())]); |
| 94 |
assert_eq!(env["PATH"], "/opt/llvm-mos/bin:/usr/bin:/bin", "the image's PATH replaces the default"); |
| 95 |
assert_eq!(env["X16EMU"], "/job", "the job's value wins"); |
| 96 |
assert_eq!(env["HOME"], "/tmp"); |
| 97 |
assert_eq!(env["LANG"], "C"); |
| 98 |
assert_eq!(job_env(None, &[])["PATH"], super::PATH); |
| 99 |
} |
| 100 |
|
| 101 |
#[test] |
| 102 |
fn dev_gets_the_usual_links_once() { |
| 103 |
let dir = std::env::temp_dir().join(format!("flow-init-dev-{}", std::process::id())); |
| 104 |
let _ = std::fs::remove_dir_all(&dir); |
| 105 |
std::fs::create_dir_all(&dir).unwrap(); |
| 106 |
let d = dir.to_str().unwrap(); |
| 107 |
dev_links(d).unwrap(); |
| 108 |
dev_links(d).unwrap(); |
| 109 |
assert_eq!(std::fs::read_link(dir.join("fd")).unwrap(), std::path::Path::new("/proc/self/fd")); |
| 110 |
assert_eq!(std::fs::read_link(dir.join("stdout")).unwrap(), std::path::Path::new("/proc/self/fd/1")); |
| 111 |
|
| 112 |
assert!(dir.join("fd").join("0").exists() || !std::path::Path::new("/proc/self/fd/0").exists()); |
| 113 |
std::fs::remove_dir_all(&dir).unwrap(); |
| 114 |
} |
| 115 |
|
| 116 |
use std::os::unix::fs::PermissionsExt; |
| 117 |
|
| 118 |
#[test] |
| 119 |
fn programs_are_found_in_the_jobs_path() { |
| 120 |
let dir = std::env::temp_dir().join(format!("flow-init-test-{}", std::process::id())); |
| 121 |
let (a, b) = (dir.join("a"), dir.join("b")); |
| 122 |
std::fs::create_dir_all(&a).unwrap(); |
| 123 |
std::fs::create_dir_all(&b).unwrap(); |
| 124 |
std::fs::write(a.join("tool"), "not executable").unwrap(); |
| 125 |
std::fs::write(b.join("tool"), "#!/bin/sh\n").unwrap(); |
| 126 |
std::fs::set_permissions(b.join("tool"), std::fs::Permissions::from_mode(0o755)).unwrap(); |
| 127 |
std::fs::create_dir_all(a.join("dir")).unwrap(); |
| 128 |
let path = format!("{}::{}", a.display(), b.display()); |
| 129 |
assert_eq!(resolve_program("tool", &path), Some(format!("{}/tool", b.display())), "skips a file that is not executable"); |
| 130 |
assert_eq!(resolve_program("dir", &path), None, "and directories"); |
| 131 |
assert_eq!(resolve_program("missing", &path), None); |
| 132 |
assert_eq!(resolve_program("./x", &path).as_deref(), Some("./x"), "a path is used as given"); |
| 133 |
assert_eq!(resolve_program("", &path), None); |
| 134 |
std::fs::remove_dir_all(&dir).unwrap(); |
| 135 |
} |
| 136 |
} |