lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

lifecycle.rs (49981B)


      1 use std::fs::{self, File, OpenOptions};
      2 use std::path::{Path, PathBuf};
      3 use std::process::{Command, ExitStatus, Output, Stdio};
      4 use std::thread;
      5 use std::time::Duration;
      6 
      7 use flate2::read::GzDecoder;
      8 
      9 use crate::error::RadrootsRuntimeManagerError;
     10 use crate::model::ManagedRuntimeInstanceRecord;
     11 use crate::paths::ManagedRuntimeInstancePaths;
     12 
     13 pub fn ensure_instance_layout(
     14     paths: &ManagedRuntimeInstancePaths,
     15 ) -> Result<(), RadrootsRuntimeManagerError> {
     16     for path in [
     17         &paths.install_dir,
     18         &paths.state_dir,
     19         &paths.logs_dir,
     20         &paths.run_dir,
     21         &paths.secrets_dir,
     22     ] {
     23         fs::create_dir_all(path).map_err(|source| {
     24             RadrootsRuntimeManagerError::CreateDirectory {
     25                 path: path.clone(),
     26                 source,
     27             }
     28         })?;
     29     }
     30     Ok(())
     31 }
     32 
     33 pub fn install_binary(
     34     source_binary_path: impl AsRef<Path>,
     35     paths: &ManagedRuntimeInstancePaths,
     36     binary_name: &str,
     37 ) -> Result<PathBuf, RadrootsRuntimeManagerError> {
     38     install_binary_path(source_binary_path.as_ref(), paths, binary_name)
     39 }
     40 
     41 fn install_binary_path(
     42     source_binary_path: &Path,
     43     paths: &ManagedRuntimeInstancePaths,
     44     binary_name: &str,
     45 ) -> Result<PathBuf, RadrootsRuntimeManagerError> {
     46     ensure_instance_layout(paths)?;
     47     let installed_binary_path = paths.install_dir.join(binary_name);
     48     fs::copy(source_binary_path, &installed_binary_path).map_err(|source| {
     49         RadrootsRuntimeManagerError::CopyBinary {
     50             from: source_binary_path.to_path_buf(),
     51             to: installed_binary_path.clone(),
     52             source,
     53         }
     54     })?;
     55     set_executable_mode(&installed_binary_path)?;
     56     Ok(installed_binary_path)
     57 }
     58 
     59 pub fn extract_binary_archive(
     60     archive_path: impl AsRef<Path>,
     61     archive_format: &str,
     62     paths: &ManagedRuntimeInstancePaths,
     63     binary_name: &str,
     64 ) -> Result<PathBuf, RadrootsRuntimeManagerError> {
     65     extract_binary_archive_path(archive_path.as_ref(), archive_format, paths, binary_name)
     66 }
     67 
     68 fn extract_binary_archive_path(
     69     archive_path: &Path,
     70     archive_format: &str,
     71     paths: &ManagedRuntimeInstancePaths,
     72     binary_name: &str,
     73 ) -> Result<PathBuf, RadrootsRuntimeManagerError> {
     74     remove_path_if_exists(&paths.install_dir)?;
     75     ensure_instance_layout(paths)?;
     76 
     77     match archive_format {
     78         "tar.gz" => unpack_tar_gz_archive(archive_path, &paths.install_dir)?,
     79         other => {
     80             return Err(RadrootsRuntimeManagerError::UnsupportedArchiveFormat {
     81                 archive_path: archive_path.to_path_buf(),
     82                 archive_format: other.to_owned(),
     83             });
     84         }
     85     }
     86 
     87     let installed_binary_path = paths.install_dir.join(binary_name);
     88     let resolved_binary_path = if installed_binary_path.is_file() {
     89         installed_binary_path
     90     } else {
     91         find_binary_with_name(&paths.install_dir, binary_name).ok_or_else(|| {
     92             RadrootsRuntimeManagerError::ReadManagedFile {
     93                 path: paths.install_dir.join(binary_name),
     94                 source: std::io::Error::new(
     95                     std::io::ErrorKind::NotFound,
     96                     format!(
     97                         "archive {} did not produce a `{binary_name}` binary under {}",
     98                         archive_path.display(),
     99                         paths.install_dir.display()
    100                     ),
    101                 ),
    102             }
    103         })?
    104     };
    105     set_executable_mode(&resolved_binary_path)?;
    106     Ok(resolved_binary_path)
    107 }
    108 
    109 pub fn write_instance_metadata(
    110     paths: &ManagedRuntimeInstancePaths,
    111     record: &ManagedRuntimeInstanceRecord,
    112 ) -> Result<(), RadrootsRuntimeManagerError> {
    113     ensure_instance_layout(paths)?;
    114     let raw = serialize_instance_metadata(record)?;
    115     fs::write(&paths.metadata_path, raw).map_err(|source| {
    116         RadrootsRuntimeManagerError::WriteInstanceMetadata {
    117             path: paths.metadata_path.clone(),
    118             source,
    119         }
    120     })
    121 }
    122 
    123 pub fn write_managed_file(
    124     path: impl AsRef<Path>,
    125     contents: &str,
    126 ) -> Result<(), RadrootsRuntimeManagerError> {
    127     write_managed_file_path(path.as_ref(), contents)
    128 }
    129 
    130 fn write_managed_file_path(path: &Path, contents: &str) -> Result<(), RadrootsRuntimeManagerError> {
    131     ensure_parent_dir(path)?;
    132     fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedFile {
    133         path: path.to_path_buf(),
    134         source,
    135     })
    136 }
    137 
    138 pub fn write_secret_file(
    139     path: impl AsRef<Path>,
    140     contents: &str,
    141 ) -> Result<(), RadrootsRuntimeManagerError> {
    142     write_secret_file_path(path.as_ref(), contents)
    143 }
    144 
    145 fn write_secret_file_path(path: &Path, contents: &str) -> Result<(), RadrootsRuntimeManagerError> {
    146     ensure_parent_dir(path)?;
    147     fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedFile {
    148         path: path.to_path_buf(),
    149         source,
    150     })?;
    151     set_secret_mode(path)?;
    152     Ok(())
    153 }
    154 
    155 pub fn read_secret_file(path: impl AsRef<Path>) -> Result<String, RadrootsRuntimeManagerError> {
    156     read_secret_file_path(path.as_ref())
    157 }
    158 
    159 fn read_secret_file_path(path: &Path) -> Result<String, RadrootsRuntimeManagerError> {
    160     fs::read_to_string(path).map_err(|source| RadrootsRuntimeManagerError::ReadManagedFile {
    161         path: path.to_path_buf(),
    162         source,
    163     })
    164 }
    165 
    166 pub fn start_process(
    167     binary_path: impl AsRef<Path>,
    168     args: &[String],
    169     envs: &[(String, String)],
    170     paths: &ManagedRuntimeInstancePaths,
    171 ) -> Result<u32, RadrootsRuntimeManagerError> {
    172     start_process_path(binary_path.as_ref(), args, envs, paths)
    173 }
    174 
    175 fn start_process_path(
    176     binary_path: &Path,
    177     args: &[String],
    178     envs: &[(String, String)],
    179     paths: &ManagedRuntimeInstancePaths,
    180 ) -> Result<u32, RadrootsRuntimeManagerError> {
    181     ensure_instance_layout(paths)?;
    182     let stdout = open_log_file(&paths.stdout_log_path)?;
    183     let stderr = open_log_file(&paths.stderr_log_path)?;
    184     let child = Command::new(binary_path)
    185         .args(args)
    186         .envs(envs.iter().map(|(key, value)| (key, value)))
    187         .stdin(Stdio::null())
    188         .stdout(Stdio::from(stdout))
    189         .stderr(Stdio::from(stderr))
    190         .spawn()
    191         .map_err(|source| RadrootsRuntimeManagerError::SpawnProcess {
    192             binary_path: binary_path.to_path_buf(),
    193             source,
    194         })?;
    195     let pid = child.id();
    196     fs::write(&paths.pid_file_path, pid.to_string()).map_err(|source| {
    197         RadrootsRuntimeManagerError::WritePidFile {
    198             path: paths.pid_file_path.clone(),
    199             source,
    200         }
    201     })?;
    202     Ok(pid)
    203 }
    204 
    205 pub fn process_running(
    206     paths: &ManagedRuntimeInstancePaths,
    207 ) -> Result<bool, RadrootsRuntimeManagerError> {
    208     let Some(pid) = read_pid(paths)? else {
    209         return Ok(false);
    210     };
    211     Ok(process_running_for_pid(pid))
    212 }
    213 
    214 pub fn stop_process(
    215     paths: &ManagedRuntimeInstancePaths,
    216 ) -> Result<bool, RadrootsRuntimeManagerError> {
    217     let Some(pid) = read_pid(paths)? else {
    218         return Ok(false);
    219     };
    220     if !process_running_for_pid(pid) {
    221         remove_pid_file(paths)?;
    222         return Ok(false);
    223     }
    224 
    225     let mut is_running = process_running_for_pid;
    226     let mut terminate = terminate_process;
    227     let mut force_kill = force_kill_process;
    228     let mut sleep = thread::sleep;
    229     stop_process_for_pid(
    230         paths,
    231         pid,
    232         &mut is_running,
    233         &mut terminate,
    234         &mut force_kill,
    235         &mut sleep,
    236     )
    237 }
    238 
    239 pub fn remove_instance_artifacts(
    240     paths: &ManagedRuntimeInstancePaths,
    241 ) -> Result<(), RadrootsRuntimeManagerError> {
    242     for path in [
    243         &paths.install_dir,
    244         &paths.state_dir,
    245         &paths.logs_dir,
    246         &paths.run_dir,
    247         &paths.secrets_dir,
    248     ] {
    249         remove_path_if_exists(path)?;
    250     }
    251     Ok(())
    252 }
    253 
    254 fn serialize_instance_metadata(
    255     record: &ManagedRuntimeInstanceRecord,
    256 ) -> Result<String, RadrootsRuntimeManagerError> {
    257     serialize_instance_metadata_with(record, toml::to_string_pretty)
    258 }
    259 
    260 fn serialize_instance_metadata_with(
    261     record: &ManagedRuntimeInstanceRecord,
    262     serializer: fn(&ManagedRuntimeInstanceRecord) -> Result<String, toml::ser::Error>,
    263 ) -> Result<String, RadrootsRuntimeManagerError> {
    264     serializer(record).map_err(|details| {
    265         RadrootsRuntimeManagerError::SerializeInstanceMetadata(details.to_string())
    266     })
    267 }
    268 
    269 fn stop_process_for_pid(
    270     paths: &ManagedRuntimeInstancePaths,
    271     pid: u32,
    272     is_running: &mut dyn FnMut(u32) -> bool,
    273     terminate: &mut dyn FnMut(u32) -> Result<(), RadrootsRuntimeManagerError>,
    274     force_kill: &mut dyn FnMut(u32) -> Result<(), RadrootsRuntimeManagerError>,
    275     sleep: &mut dyn FnMut(Duration),
    276 ) -> Result<bool, RadrootsRuntimeManagerError> {
    277     terminate(pid)?;
    278     for _ in 0..20 {
    279         if !is_running(pid) {
    280             remove_pid_file(paths)?;
    281             return Ok(true);
    282         }
    283         sleep(Duration::from_millis(100));
    284     }
    285 
    286     force_kill(pid)?;
    287     for _ in 0..20 {
    288         if !is_running(pid) {
    289             remove_pid_file(paths)?;
    290             return Ok(true);
    291         }
    292         sleep(Duration::from_millis(100));
    293     }
    294 
    295     Err(RadrootsRuntimeManagerError::StopProcess {
    296         pid,
    297         details: "process did not exit after terminate and force-kill attempts".to_owned(),
    298     })
    299 }
    300 
    301 fn unpack_tar_gz_archive(
    302     archive_path: &Path,
    303     destination_dir: &Path,
    304 ) -> Result<(), RadrootsRuntimeManagerError> {
    305     let archive_file = File::open(archive_path).map_err(|source| {
    306         RadrootsRuntimeManagerError::ReadManagedFile {
    307             path: archive_path.to_path_buf(),
    308             source,
    309         }
    310     })?;
    311     let decoder = GzDecoder::new(archive_file);
    312     let mut archive = tar::Archive::new(decoder);
    313     archive
    314         .unpack(destination_dir)
    315         .map_err(|source| RadrootsRuntimeManagerError::UnpackArchive {
    316             archive_path: archive_path.to_path_buf(),
    317             source,
    318         })
    319 }
    320 
    321 fn find_binary_with_name(root: &Path, binary_name: &str) -> Option<PathBuf> {
    322     let entries = fs::read_dir(root).ok()?;
    323     for entry in entries.flatten() {
    324         let path = entry.path();
    325         if path.is_dir() {
    326             if let Some(found) = find_binary_with_name(&path, binary_name) {
    327                 return Some(found);
    328             }
    329             continue;
    330         }
    331         if path.file_name().and_then(|name| name.to_str()) == Some(binary_name) {
    332             return Some(path);
    333         }
    334     }
    335     None
    336 }
    337 
    338 fn open_log_file(path: &Path) -> Result<File, RadrootsRuntimeManagerError> {
    339     ensure_parent_dir(path)?;
    340     OpenOptions::new()
    341         .create(true)
    342         .append(true)
    343         .open(path)
    344         .map_err(|source| RadrootsRuntimeManagerError::OpenLogFile {
    345             path: path.to_path_buf(),
    346             source,
    347         })
    348 }
    349 
    350 fn ensure_parent_dir(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    351     let Some(parent) = path.parent() else {
    352         return Ok(());
    353     };
    354     fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateDirectory {
    355         path: parent.to_path_buf(),
    356         source,
    357     })
    358 }
    359 
    360 fn read_pid(
    361     paths: &ManagedRuntimeInstancePaths,
    362 ) -> Result<Option<u32>, RadrootsRuntimeManagerError> {
    363     let raw = match fs::read_to_string(&paths.pid_file_path) {
    364         Ok(raw) => raw,
    365         Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
    366         Err(source) => {
    367             return Err(RadrootsRuntimeManagerError::ReadPidFile {
    368                 path: paths.pid_file_path.clone(),
    369                 source,
    370             });
    371         }
    372     };
    373     let trimmed = raw.trim();
    374     if trimmed.is_empty() {
    375         return Ok(None);
    376     }
    377     trimmed
    378         .parse::<u32>()
    379         .map(Some)
    380         .map_err(|_| RadrootsRuntimeManagerError::ParsePidFile {
    381             path: paths.pid_file_path.clone(),
    382             contents: trimmed.to_owned(),
    383         })
    384 }
    385 
    386 fn remove_pid_file(paths: &ManagedRuntimeInstancePaths) -> Result<(), RadrootsRuntimeManagerError> {
    387     match fs::remove_file(&paths.pid_file_path) {
    388         Ok(()) => Ok(()),
    389         Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
    390         Err(source) => Err(RadrootsRuntimeManagerError::RemovePath {
    391             path: paths.pid_file_path.clone(),
    392             source,
    393         }),
    394     }
    395 }
    396 
    397 fn remove_path_if_exists(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    398     let state = match fs::metadata(path) {
    399         Ok(metadata) if metadata.is_dir() => Ok(Some(ExistingPathKind::Directory)),
    400         Ok(_) => Ok(Some(ExistingPathKind::File)),
    401         Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
    402         Err(source) => Err(source),
    403     };
    404     remove_path_from_state(path, state, remove_dir_all_path, remove_file_path)
    405 }
    406 
    407 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    408 enum ExistingPathKind {
    409     Directory,
    410     File,
    411 }
    412 
    413 fn remove_path_from_state(
    414     path: &Path,
    415     state: Result<Option<ExistingPathKind>, std::io::Error>,
    416     remove_dir_all: fn(&Path) -> std::io::Result<()>,
    417     remove_file: fn(&Path) -> std::io::Result<()>,
    418 ) -> Result<(), RadrootsRuntimeManagerError> {
    419     match state {
    420         Ok(Some(ExistingPathKind::Directory)) => {
    421             remove_dir_all(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath {
    422                 path: path.to_path_buf(),
    423                 source,
    424             })
    425         }
    426         Ok(Some(ExistingPathKind::File)) => {
    427             remove_file(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath {
    428                 path: path.to_path_buf(),
    429                 source,
    430             })
    431         }
    432         Ok(None) => Ok(()),
    433         Err(source) => Err(RadrootsRuntimeManagerError::ReadManagedFile {
    434             path: path.to_path_buf(),
    435             source,
    436         }),
    437     }
    438 }
    439 
    440 fn remove_dir_all_path(path: &Path) -> std::io::Result<()> {
    441     fs::remove_dir_all(path)
    442 }
    443 
    444 fn remove_file_path(path: &Path) -> std::io::Result<()> {
    445     fs::remove_file(path)
    446 }
    447 
    448 #[cfg(unix)]
    449 fn set_executable_mode(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    450     apply_mode(path, 0o755, set_permissions_path)
    451 }
    452 
    453 #[cfg(not(unix))]
    454 fn set_executable_mode(_path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    455     Ok(())
    456 }
    457 
    458 #[cfg(unix)]
    459 fn set_secret_mode(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    460     apply_mode(path, 0o600, set_permissions_path)
    461 }
    462 
    463 #[cfg(unix)]
    464 fn apply_mode(
    465     path: &Path,
    466     mode: u32,
    467     set_permissions: fn(&Path, fs::Permissions) -> std::io::Result<()>,
    468 ) -> Result<(), RadrootsRuntimeManagerError> {
    469     use std::os::unix::fs::PermissionsExt;
    470 
    471     let metadata =
    472         fs::metadata(path).map_err(|source| RadrootsRuntimeManagerError::ReadManagedFile {
    473             path: path.to_path_buf(),
    474             source,
    475         })?;
    476     let mut permissions = metadata.permissions();
    477     permissions.set_mode(mode);
    478     set_permissions(path, permissions).map_err(|source| {
    479         RadrootsRuntimeManagerError::SetPermissions {
    480             path: path.to_path_buf(),
    481             source,
    482         }
    483     })
    484 }
    485 
    486 #[cfg(unix)]
    487 fn set_permissions_path(path: &Path, permissions: fs::Permissions) -> std::io::Result<()> {
    488     fs::set_permissions(path, permissions)
    489 }
    490 
    491 #[cfg(not(unix))]
    492 fn set_secret_mode(_path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    493     Ok(())
    494 }
    495 
    496 #[cfg(unix)]
    497 fn process_running_for_pid(pid: u32) -> bool {
    498     let pid_arg = pid.to_string();
    499     let running = Command::new("kill")
    500         .args(["-0", pid_arg.as_str()])
    501         .stdout(Stdio::null())
    502         .stderr(Stdio::null())
    503         .status()
    504         .map(|status| status.success())
    505         .unwrap_or(false);
    506     if !running {
    507         return false;
    508     }
    509 
    510     ps_output_for_pid(pid_arg.as_str())
    511         .map(process_running_state_from_ps_output)
    512         .unwrap_or(true)
    513 }
    514 
    515 #[cfg(unix)]
    516 fn ps_output_for_pid(pid_arg: &str) -> std::io::Result<Output> {
    517     Command::new("ps")
    518         .args(["-o", "stat=", "-p", pid_arg])
    519         .stdout(Stdio::piped())
    520         .stderr(Stdio::null())
    521         .output()
    522 }
    523 
    524 #[cfg(unix)]
    525 fn process_running_state_from_ps_output(output: Output) -> bool {
    526     if !output.status.success() {
    527         return true;
    528     }
    529     let state = String::from_utf8_lossy(output.stdout.as_slice());
    530     !state.trim_start().starts_with('Z')
    531 }
    532 
    533 #[cfg(windows)]
    534 fn process_running_for_pid(pid: u32) -> bool {
    535     Command::new("tasklist")
    536         .args(["/FI", format!("PID eq {pid}").as_str()])
    537         .stdout(Stdio::piped())
    538         .stderr(Stdio::null())
    539         .output()
    540         .map(|output| {
    541             output.status.success()
    542                 && String::from_utf8_lossy(output.stdout.as_slice())
    543                     .contains(pid.to_string().as_str())
    544         })
    545         .unwrap_or(false)
    546 }
    547 
    548 #[cfg(not(any(unix, windows)))]
    549 fn process_running_for_pid(_pid: u32) -> bool {
    550     false
    551 }
    552 
    553 #[cfg(unix)]
    554 fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    555     signal_process(pid, "-TERM")
    556 }
    557 
    558 #[cfg(unix)]
    559 fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    560     signal_process(pid, "-KILL")
    561 }
    562 
    563 #[cfg(unix)]
    564 fn signal_process(pid: u32, signal: &str) -> Result<(), RadrootsRuntimeManagerError> {
    565     signal_process_with(pid, signal, execute_signal_command)
    566 }
    567 
    568 #[cfg(unix)]
    569 fn execute_signal_command(pid: u32, signal: &str) -> std::io::Result<ExitStatus> {
    570     Command::new("kill")
    571         .args([signal, pid.to_string().as_str()])
    572         .stdout(Stdio::null())
    573         .stderr(Stdio::piped())
    574         .status()
    575 }
    576 
    577 #[cfg(unix)]
    578 fn signal_process_with(
    579     pid: u32,
    580     signal: &str,
    581     runner: fn(u32, &str) -> std::io::Result<ExitStatus>,
    582 ) -> Result<(), RadrootsRuntimeManagerError> {
    583     let status = runner(pid, signal).map_err(|source| {
    584         RadrootsRuntimeManagerError::ExecuteProcessSignal {
    585             pid,
    586             signal: signal.to_owned(),
    587             source,
    588         }
    589     })?;
    590     if status.success() {
    591         Ok(())
    592     } else {
    593         Err(RadrootsRuntimeManagerError::StopProcess {
    594             pid,
    595             details: format!("`kill {signal}` returned {status}"),
    596         })
    597     }
    598 }
    599 
    600 #[cfg(windows)]
    601 fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    602     force_kill_process(pid)
    603 }
    604 
    605 #[cfg(windows)]
    606 fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    607     let status = Command::new("taskkill")
    608         .args(["/PID", pid.to_string().as_str(), "/T", "/F"])
    609         .stdout(Stdio::null())
    610         .stderr(Stdio::null())
    611         .status()
    612         .map_err(|source| RadrootsRuntimeManagerError::ExecuteProcessSignal {
    613             pid,
    614             signal: "taskkill".to_owned(),
    615             source,
    616         })?;
    617     if status.success() {
    618         Ok(())
    619     } else {
    620         Err(RadrootsRuntimeManagerError::StopProcess {
    621             pid,
    622             details: format!("`taskkill` returned {status}"),
    623         })
    624     }
    625 }
    626 
    627 #[cfg(not(any(unix, windows)))]
    628 fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    629     Err(RadrootsRuntimeManagerError::StopProcess {
    630         pid,
    631         details: "process signaling is unsupported on this platform".to_owned(),
    632     })
    633 }
    634 
    635 #[cfg(not(any(unix, windows)))]
    636 fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    637     Err(RadrootsRuntimeManagerError::StopProcess {
    638         pid,
    639         details: "process signaling is unsupported on this platform".to_owned(),
    640     })
    641 }
    642 
    643 #[cfg(test)]
    644 mod tests {
    645     use std::fs;
    646     use std::fs::File;
    647     use std::io;
    648     use std::path::Path;
    649     use std::process::ExitStatus;
    650     use std::thread;
    651     use std::time::Duration;
    652 
    653     #[cfg(unix)]
    654     use std::os::unix::fs::PermissionsExt;
    655 
    656     #[cfg(unix)]
    657     use serde::ser::Error as _;
    658     use tempfile::tempdir;
    659 
    660     use super::{
    661         ExistingPathKind, apply_mode, ensure_instance_layout, ensure_parent_dir,
    662         extract_binary_archive, find_binary_with_name, force_kill_process, install_binary,
    663         open_log_file, process_running, process_running_for_pid,
    664         process_running_state_from_ps_output, read_pid, read_secret_file,
    665         remove_instance_artifacts, remove_path_from_state, remove_path_if_exists,
    666         serialize_instance_metadata_with, set_executable_mode, set_secret_mode, signal_process,
    667         signal_process_with, start_process, stop_process, stop_process_for_pid, terminate_process,
    668         write_instance_metadata, write_managed_file, write_secret_file,
    669     };
    670     use crate::error::RadrootsRuntimeManagerError;
    671     use crate::model::{ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord};
    672     use crate::paths::ManagedRuntimeInstancePaths;
    673 
    674     fn sample_paths(root: &Path) -> ManagedRuntimeInstancePaths {
    675         ManagedRuntimeInstancePaths {
    676             install_dir: root.join("install"),
    677             state_dir: root.join("state"),
    678             logs_dir: root.join("logs"),
    679             run_dir: root.join("run"),
    680             secrets_dir: root.join("secrets"),
    681             pid_file_path: root.join("run/runtime.pid"),
    682             stdout_log_path: root.join("logs/stdout.log"),
    683             stderr_log_path: root.join("logs/stderr.log"),
    684             metadata_path: root.join("state/instance.toml"),
    685         }
    686     }
    687 
    688     fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) {
    689         let rendered = err.to_string();
    690         for part in parts {
    691             assert!(
    692                 rendered.contains(part),
    693                 "expected `{rendered}` to contain `{part}`"
    694             );
    695         }
    696     }
    697 
    698     fn sample_record(paths: &ManagedRuntimeInstancePaths) -> ManagedRuntimeInstanceRecord {
    699         ManagedRuntimeInstanceRecord {
    700             runtime_id: "radrootsd".to_owned(),
    701             instance_id: "local".to_owned(),
    702             management_mode: "interactive_user_managed".to_owned(),
    703             install_state: ManagedRuntimeInstallState::Configured,
    704             binary_path: paths.install_dir.join("radrootsd"),
    705             config_path: paths.state_dir.join("config.toml"),
    706             logs_path: paths.logs_dir.clone(),
    707             run_path: paths.run_dir.clone(),
    708             installed_version: "0.1.0".to_owned(),
    709             health_endpoint: Some("http://127.0.0.1:7070".to_owned()),
    710             secret_material_ref: Some(paths.secrets_dir.join("token.txt").display().to_string()),
    711             last_started_at: None,
    712             last_stopped_at: None,
    713             notes: Some("test".to_owned()),
    714         }
    715     }
    716 
    717     #[cfg(unix)]
    718     fn exit_status(code: i32) -> ExitStatus {
    719         std::process::Command::new("sh")
    720             .args(["-c", &format!("exit {code}")])
    721             .status()
    722             .expect("exit status")
    723     }
    724 
    725     #[cfg(unix)]
    726     fn output_with_status(status: ExitStatus, stdout: &[u8]) -> std::process::Output {
    727         std::process::Output {
    728             status,
    729             stdout: stdout.to_vec(),
    730             stderr: Vec::new(),
    731         }
    732     }
    733 
    734     fn ok_remove_path(_path: &Path) -> io::Result<()> {
    735         Ok(())
    736     }
    737 
    738     fn deny_remove_path(_path: &Path) -> io::Result<()> {
    739         Err(io::Error::new(
    740             io::ErrorKind::PermissionDenied,
    741             "remove path denied",
    742         ))
    743     }
    744 
    745     fn ok_runtime_signal(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
    746         Ok(())
    747     }
    748 
    749     fn noop_runtime_sleep(_duration: Duration) {}
    750 
    751     fn runtime_is_stopped(_pid: u32) -> bool {
    752         false
    753     }
    754 
    755     fn runtime_is_running(_pid: u32) -> bool {
    756         true
    757     }
    758 
    759     #[test]
    760     fn layout_and_metadata_helpers_write_expected_files() {
    761         let dir = tempdir().expect("tempdir");
    762         let paths = sample_paths(dir.path());
    763         ensure_instance_layout(&paths).expect("layout");
    764         write_managed_file(paths.state_dir.join("config.toml"), "value = true").expect("config");
    765         write_secret_file(paths.secrets_dir.join("token.txt"), "secret").expect("secret");
    766         write_instance_metadata(&paths, &sample_record(&paths)).expect("metadata");
    767         assert_eq!(
    768             read_secret_file(paths.secrets_dir.join("token.txt")).expect("read secret"),
    769             "secret"
    770         );
    771         assert!(paths.metadata_path.is_file());
    772         assert!(paths.state_dir.join("config.toml").is_file());
    773     }
    774 
    775     #[test]
    776     fn install_binary_copies_source_into_install_dir() {
    777         let dir = tempdir().expect("tempdir");
    778         let source = dir.path().join("radrootsd");
    779         fs::write(&source, "#!/bin/sh\nexit 0\n").expect("source");
    780         let paths = sample_paths(dir.path());
    781         let installed = install_binary(&source, &paths, "radrootsd").expect("install");
    782         assert!(installed.is_file());
    783     }
    784 
    785     #[cfg(unix)]
    786     #[test]
    787     fn extract_binary_archive_unpacks_tar_gz() {
    788         let dir = tempdir().expect("tempdir");
    789         let archive_root = dir.path().join("archive");
    790         fs::create_dir_all(archive_root.join("bin")).expect("archive dir");
    791         fs::write(archive_root.join("bin/radrootsd"), "#!/bin/sh\nexit 0\n").expect("binary");
    792         let archive_path = dir.path().join("radrootsd.tar.gz");
    793         let file = File::create(&archive_path).expect("archive file");
    794         let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
    795         let mut builder = tar::Builder::new(encoder);
    796         builder
    797             .append_path_with_name(
    798                 archive_root.join("bin/radrootsd"),
    799                 "radrootsd/bin/radrootsd",
    800             )
    801             .expect("append path");
    802         builder.finish().expect("finish archive");
    803         let encoder = builder.into_inner().expect("into encoder");
    804         encoder.finish().expect("finish gzip");
    805 
    806         let paths = sample_paths(dir.path());
    807         let installed =
    808             extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd").expect("extract");
    809         assert!(installed.is_file());
    810     }
    811 
    812     #[cfg(unix)]
    813     #[test]
    814     fn extract_binary_archive_uses_direct_binary_when_present_at_root() {
    815         let dir = tempdir().expect("tempdir");
    816         let archive_root = dir.path().join("archive");
    817         fs::create_dir_all(&archive_root).expect("archive dir");
    818         fs::write(archive_root.join("radrootsd"), "#!/bin/sh\nexit 0\n").expect("binary");
    819         let archive_path = dir.path().join("radrootsd.tar.gz");
    820         let file = File::create(&archive_path).expect("archive file");
    821         let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
    822         let mut builder = tar::Builder::new(encoder);
    823         builder
    824             .append_path_with_name(archive_root.join("radrootsd"), "radrootsd")
    825             .expect("append path");
    826         builder.finish().expect("finish archive");
    827         let encoder = builder.into_inner().expect("into encoder");
    828         encoder.finish().expect("finish gzip");
    829 
    830         let paths = sample_paths(dir.path());
    831         let installed =
    832             extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd").expect("extract");
    833         assert_eq!(installed, paths.install_dir.join("radrootsd"));
    834     }
    835 
    836     #[cfg(unix)]
    837     #[test]
    838     fn start_and_stop_process_manage_pid_file() {
    839         let dir = tempdir().expect("tempdir");
    840         let binary = dir.path().join("sleepy.sh");
    841         fs::write(&binary, "#!/bin/sh\nexec sleep 30\n").expect("script");
    842         let paths = sample_paths(dir.path());
    843         let installed = install_binary(&binary, &paths, "sleepy.sh").expect("install");
    844         let envs = vec![("RADROOTS_RUNTIME_MANAGER_TEST".to_owned(), "1".to_owned())];
    845         let pid = start_process(&installed, &Vec::new(), &envs, &paths).expect("start");
    846         assert!(pid > 0);
    847         thread::sleep(Duration::from_millis(100));
    848         assert!(paths.pid_file_path.is_file());
    849         assert!(process_running(&paths).expect("running"));
    850         assert!(stop_process(&paths).expect("stop"));
    851         assert!(!paths.pid_file_path.exists());
    852     }
    853 
    854     #[test]
    855     fn remove_instance_artifacts_removes_layout_roots() {
    856         let dir = tempdir().expect("tempdir");
    857         let paths = sample_paths(dir.path());
    858         ensure_instance_layout(&paths).expect("layout");
    859         remove_instance_artifacts(&paths).expect("remove");
    860         assert!(!paths.install_dir.exists());
    861         assert!(!paths.state_dir.exists());
    862         assert!(!paths.logs_dir.exists());
    863         assert!(!paths.run_dir.exists());
    864         assert!(!paths.secrets_dir.exists());
    865     }
    866 
    867     #[test]
    868     fn ensure_instance_layout_reports_directory_errors() {
    869         let dir = tempdir().expect("tempdir");
    870         let paths = sample_paths(dir.path());
    871         fs::write(&paths.install_dir, "occupied").expect("file");
    872 
    873         let err = ensure_instance_layout(&paths).expect_err("file path should fail");
    874         assert_error_contains(
    875             &err,
    876             &[
    877                 paths.install_dir.to_string_lossy().as_ref(),
    878                 "create directory",
    879             ],
    880         );
    881     }
    882 
    883     #[test]
    884     fn install_binary_reports_copy_errors() {
    885         let dir = tempdir().expect("tempdir");
    886         let paths = sample_paths(dir.path());
    887         let err = install_binary(dir.path().join("missing"), &paths, "radrootsd")
    888             .expect_err("missing source should fail");
    889         assert_error_contains(
    890             &err,
    891             &[
    892                 dir.path().join("missing").to_string_lossy().as_ref(),
    893                 paths
    894                     .install_dir
    895                     .join("radrootsd")
    896                     .to_string_lossy()
    897                     .as_ref(),
    898                 "copy runtime binary",
    899             ],
    900         );
    901     }
    902 
    903     #[test]
    904     fn extract_binary_archive_reports_unsupported_format() {
    905         let dir = tempdir().expect("tempdir");
    906         let paths = sample_paths(dir.path());
    907         let archive_path = dir.path().join("radrootsd.zip");
    908 
    909         let err = extract_binary_archive(&archive_path, "zip", &paths, "radrootsd")
    910             .expect_err("unsupported archive format should fail");
    911         assert_error_contains(&err, &[archive_path.to_string_lossy().as_ref(), "zip"]);
    912     }
    913 
    914     #[test]
    915     fn extract_binary_archive_reports_missing_archive() {
    916         let dir = tempdir().expect("tempdir");
    917         let paths = sample_paths(dir.path());
    918         let archive_path = dir.path().join("missing.tar.gz");
    919 
    920         let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
    921             .expect_err("missing archive should fail");
    922         assert_error_contains(
    923             &err,
    924             &[archive_path.to_string_lossy().as_ref(), "read managed file"],
    925         );
    926     }
    927 
    928     #[cfg(unix)]
    929     #[test]
    930     fn extract_binary_archive_reports_missing_binary_in_archive() {
    931         let dir = tempdir().expect("tempdir");
    932         let archive_root = dir.path().join("archive");
    933         fs::create_dir_all(archive_root.join("bin")).expect("archive dir");
    934         fs::write(archive_root.join("bin/other"), "#!/bin/sh\nexit 0\n").expect("binary");
    935         let archive_path = dir.path().join("radrootsd.tar.gz");
    936         let file = File::create(&archive_path).expect("archive file");
    937         let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
    938         let mut builder = tar::Builder::new(encoder);
    939         builder
    940             .append_path_with_name(archive_root.join("bin/other"), "radrootsd/bin/other")
    941             .expect("append path");
    942         builder.finish().expect("finish archive");
    943         let encoder = builder.into_inner().expect("into encoder");
    944         encoder.finish().expect("finish gzip");
    945 
    946         let paths = sample_paths(dir.path());
    947         let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
    948             .expect_err("archive should not resolve missing binary");
    949         assert_error_contains(
    950             &err,
    951             &[
    952                 paths
    953                     .install_dir
    954                     .join("radrootsd")
    955                     .to_string_lossy()
    956                     .as_ref(),
    957                 "did not produce",
    958             ],
    959         );
    960     }
    961 
    962     #[cfg(unix)]
    963     #[test]
    964     fn extract_binary_archive_reports_unpack_errors() {
    965         let dir = tempdir().expect("tempdir");
    966         let archive_path = dir.path().join("invalid.tar.gz");
    967         fs::write(&archive_path, "not a gzip archive").expect("write archive");
    968         let paths = sample_paths(dir.path());
    969 
    970         let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
    971             .expect_err("invalid archive should fail");
    972         assert_error_contains(
    973             &err,
    974             &[archive_path.to_string_lossy().as_ref(), "unpack archive"],
    975         );
    976     }
    977 
    978     #[test]
    979     fn write_managed_file_reports_write_errors() {
    980         let dir = tempdir().expect("tempdir");
    981         let path = dir.path().join("as-dir");
    982         fs::create_dir(&path).expect("create directory target");
    983 
    984         let err = write_managed_file(&path, "value").expect_err("directory write should fail");
    985         assert_error_contains(
    986             &err,
    987             &[path.to_string_lossy().as_ref(), "write managed file"],
    988         );
    989     }
    990 
    991     #[test]
    992     fn write_secret_file_reports_write_errors() {
    993         let dir = tempdir().expect("tempdir");
    994         let path = dir.path().join("as-dir");
    995         fs::create_dir(&path).expect("create directory target");
    996 
    997         let err = write_secret_file(&path, "secret").expect_err("directory write should fail");
    998         assert_error_contains(
    999             &err,
   1000             &[path.to_string_lossy().as_ref(), "write managed file"],
   1001         );
   1002     }
   1003 
   1004     #[test]
   1005     fn read_secret_file_reports_missing_path() {
   1006         let dir = tempdir().expect("tempdir");
   1007         let path = dir.path().join("missing.secret");
   1008         let err = read_secret_file(&path).expect_err("missing secret should fail");
   1009         assert_error_contains(
   1010             &err,
   1011             &[path.to_string_lossy().as_ref(), "read managed file"],
   1012         );
   1013     }
   1014 
   1015     #[test]
   1016     fn write_instance_metadata_reports_write_errors() {
   1017         let dir = tempdir().expect("tempdir");
   1018         let mut paths = sample_paths(dir.path());
   1019         ensure_instance_layout(&paths).expect("layout");
   1020         fs::create_dir_all(&paths.metadata_path).expect("create metadata dir");
   1021         paths.metadata_path = paths.metadata_path.clone();
   1022 
   1023         let err = write_instance_metadata(
   1024             &paths,
   1025             &ManagedRuntimeInstanceRecord {
   1026                 health_endpoint: None,
   1027                 secret_material_ref: None,
   1028                 notes: None,
   1029                 ..sample_record(&paths)
   1030             },
   1031         )
   1032         .expect_err("metadata dir target should fail");
   1033         assert_error_contains(
   1034             &err,
   1035             &[
   1036                 paths.metadata_path.to_string_lossy().as_ref(),
   1037                 "write runtime instance metadata",
   1038             ],
   1039         );
   1040     }
   1041 
   1042     #[test]
   1043     fn serialize_instance_metadata_reports_serializer_errors() {
   1044         let dir = tempdir().expect("tempdir");
   1045         let paths = sample_paths(dir.path());
   1046         let err = serialize_instance_metadata_with(&sample_record(&paths), |_| {
   1047             Err(toml::ser::Error::custom("forced serializer failure"))
   1048         })
   1049         .expect_err("serializer should fail");
   1050         assert_error_contains(
   1051             &err,
   1052             &[
   1053                 "serialize runtime instance metadata",
   1054                 "forced serializer failure",
   1055             ],
   1056         );
   1057     }
   1058 
   1059     #[test]
   1060     fn start_process_reports_spawn_errors() {
   1061         let dir = tempdir().expect("tempdir");
   1062         let paths = sample_paths(dir.path());
   1063         let err = start_process(dir.path().join("missing"), &[], &[], &paths)
   1064             .expect_err("missing binary should fail");
   1065         assert_error_contains(
   1066             &err,
   1067             &[
   1068                 dir.path().join("missing").to_string_lossy().as_ref(),
   1069                 "spawn managed runtime process",
   1070             ],
   1071         );
   1072     }
   1073 
   1074     #[cfg(unix)]
   1075     #[test]
   1076     fn start_process_reports_pid_file_write_errors() {
   1077         let dir = tempdir().expect("tempdir");
   1078         let binary = dir.path().join("sleepy.sh");
   1079         fs::write(&binary, "#!/bin/sh\nexec sleep 1\n").expect("script");
   1080         let mut paths = sample_paths(dir.path());
   1081         paths.pid_file_path = paths.run_dir.clone();
   1082         let installed =
   1083             install_binary(&binary, &sample_paths(dir.path()), "sleepy.sh").expect("install");
   1084 
   1085         let err =
   1086             start_process(&installed, &[], &[], &paths).expect_err("pid file write should fail");
   1087         assert_error_contains(
   1088             &err,
   1089             &[paths.run_dir.to_string_lossy().as_ref(), "write pid file"],
   1090         );
   1091     }
   1092 
   1093     #[test]
   1094     fn process_running_and_stop_process_handle_missing_pid_file() {
   1095         let dir = tempdir().expect("tempdir");
   1096         let paths = sample_paths(dir.path());
   1097 
   1098         assert!(!process_running(&paths).expect("missing pid should be false"));
   1099         assert!(!stop_process(&paths).expect("missing pid stop should be false"));
   1100     }
   1101 
   1102     #[test]
   1103     fn process_running_reports_invalid_pid_file() {
   1104         let dir = tempdir().expect("tempdir");
   1105         let paths = sample_paths(dir.path());
   1106         ensure_instance_layout(&paths).expect("layout");
   1107         fs::write(&paths.pid_file_path, "not-a-pid").expect("write pid");
   1108 
   1109         let err = process_running(&paths).expect_err("invalid pid should fail");
   1110         assert_error_contains(
   1111             &err,
   1112             &[paths.pid_file_path.to_string_lossy().as_ref(), "not-a-pid"],
   1113         );
   1114     }
   1115 
   1116     #[test]
   1117     fn stop_process_clears_stale_pid_file() {
   1118         let dir = tempdir().expect("tempdir");
   1119         let paths = sample_paths(dir.path());
   1120         ensure_instance_layout(&paths).expect("layout");
   1121         fs::write(&paths.pid_file_path, "999999").expect("write pid");
   1122 
   1123         assert!(!stop_process(&paths).expect("stale pid should return false"));
   1124         assert!(!paths.pid_file_path.exists());
   1125     }
   1126 
   1127     #[test]
   1128     fn stop_process_for_pid_uses_force_kill_after_terminate_attempts() {
   1129         let dir = tempdir().expect("tempdir");
   1130         let paths = sample_paths(dir.path());
   1131         ensure_instance_layout(&paths).expect("layout");
   1132         fs::write(&paths.pid_file_path, "42").expect("write pid");
   1133 
   1134         let mut polls = 0_u32;
   1135         let mut is_running = |_pid| {
   1136             polls += 1;
   1137             polls <= 20
   1138         };
   1139         let mut terminate = ok_runtime_signal;
   1140         let mut force_kill = ok_runtime_signal;
   1141         let mut sleep = noop_runtime_sleep;
   1142         let stopped = stop_process_for_pid(
   1143             &paths,
   1144             42,
   1145             &mut is_running,
   1146             &mut terminate,
   1147             &mut force_kill,
   1148             &mut sleep,
   1149         )
   1150         .expect("force-kill path should stop");
   1151 
   1152         assert!(stopped);
   1153         assert!(!paths.pid_file_path.exists());
   1154         assert_eq!(polls, 21);
   1155     }
   1156 
   1157     #[test]
   1158     fn stop_process_for_pid_stops_after_terminate_poll() {
   1159         let dir = tempdir().expect("tempdir");
   1160         let paths = sample_paths(dir.path());
   1161         ensure_instance_layout(&paths).expect("layout");
   1162         fs::write(&paths.pid_file_path, "42").expect("write pid");
   1163 
   1164         let mut is_running = runtime_is_stopped;
   1165         let mut terminate = ok_runtime_signal;
   1166         let mut force_kill = ok_runtime_signal;
   1167         let mut sleep = noop_runtime_sleep;
   1168         let stopped = stop_process_for_pid(
   1169             &paths,
   1170             42,
   1171             &mut is_running,
   1172             &mut terminate,
   1173             &mut force_kill,
   1174             &mut sleep,
   1175         )
   1176         .expect("terminate poll should stop");
   1177 
   1178         assert!(stopped);
   1179         assert!(!paths.pid_file_path.exists());
   1180     }
   1181 
   1182     #[test]
   1183     fn stop_process_for_pid_reports_failure_after_force_kill_attempts() {
   1184         let dir = tempdir().expect("tempdir");
   1185         let paths = sample_paths(dir.path());
   1186         ensure_instance_layout(&paths).expect("layout");
   1187         fs::write(&paths.pid_file_path, "42").expect("write pid");
   1188 
   1189         let mut sleeps = 0_u32;
   1190         let mut is_running = runtime_is_running;
   1191         let mut terminate = ok_runtime_signal;
   1192         let mut force_kill = ok_runtime_signal;
   1193         let mut sleep = |_duration| {
   1194             sleeps += 1;
   1195         };
   1196         let err = stop_process_for_pid(
   1197             &paths,
   1198             42,
   1199             &mut is_running,
   1200             &mut terminate,
   1201             &mut force_kill,
   1202             &mut sleep,
   1203         )
   1204         .expect_err("force-kill exhaustion should fail");
   1205 
   1206         assert_error_contains(&err, &["42", "did not exit after terminate and force-kill"]);
   1207         assert_eq!(sleeps, 40);
   1208         assert!(paths.pid_file_path.exists());
   1209     }
   1210 
   1211     #[test]
   1212     fn ensure_parent_dir_without_parent_is_a_noop() {
   1213         ensure_parent_dir(Path::new("/")).expect("root path should have no parent");
   1214     }
   1215 
   1216     #[test]
   1217     fn ensure_parent_dir_reports_directory_creation_errors() {
   1218         let dir = tempdir().expect("tempdir");
   1219         let file_parent = dir.path().join("occupied");
   1220         fs::write(&file_parent, "file").expect("parent file");
   1221 
   1222         let err =
   1223             ensure_parent_dir(&file_parent.join("child")).expect_err("file parent should fail");
   1224         assert_error_contains(
   1225             &err,
   1226             &[file_parent.to_string_lossy().as_ref(), "create directory"],
   1227         );
   1228     }
   1229 
   1230     #[test]
   1231     fn find_binary_with_name_handles_nested_and_missing_files() {
   1232         let dir = tempdir().expect("tempdir");
   1233         fs::create_dir_all(dir.path().join("nested")).expect("nested dir");
   1234         fs::write(dir.path().join("nested/radrootsd"), "binary").expect("binary");
   1235 
   1236         assert_eq!(
   1237             find_binary_with_name(dir.path(), "radrootsd"),
   1238             Some(dir.path().join("nested/radrootsd"))
   1239         );
   1240         assert_eq!(find_binary_with_name(dir.path(), "missing"), None);
   1241     }
   1242 
   1243     #[test]
   1244     fn open_log_file_creates_file_and_reports_directory_errors() {
   1245         let dir = tempdir().expect("tempdir");
   1246         let file_path = dir.path().join("logs/stdout.log");
   1247         let file = open_log_file(&file_path).expect("open log");
   1248         drop(file);
   1249         assert!(file_path.is_file());
   1250 
   1251         let bad_path = dir.path().join("bad");
   1252         fs::create_dir(&bad_path).expect("create dir");
   1253         let err = open_log_file(&bad_path).expect_err("directory open should fail");
   1254         assert_error_contains(
   1255             &err,
   1256             &[bad_path.to_string_lossy().as_ref(), "open runtime log file"],
   1257         );
   1258     }
   1259 
   1260     #[test]
   1261     fn read_pid_handles_empty_missing_and_read_error_cases() {
   1262         let dir = tempdir().expect("tempdir");
   1263         let mut paths = sample_paths(dir.path());
   1264 
   1265         assert_eq!(read_pid(&paths).expect("missing pid"), None);
   1266 
   1267         ensure_instance_layout(&paths).expect("layout");
   1268         fs::write(&paths.pid_file_path, "   ").expect("write pid");
   1269         assert_eq!(read_pid(&paths).expect("empty pid"), None);
   1270 
   1271         paths.pid_file_path = paths.run_dir.clone();
   1272         let err = read_pid(&paths).expect_err("directory pid file should fail");
   1273         assert_error_contains(
   1274             &err,
   1275             &[paths.run_dir.to_string_lossy().as_ref(), "read pid file"],
   1276         );
   1277     }
   1278 
   1279     #[test]
   1280     fn remove_path_if_exists_handles_files_directories_and_missing_paths() {
   1281         let dir = tempdir().expect("tempdir");
   1282         let file_path = dir.path().join("file.txt");
   1283         let dir_path = dir.path().join("subdir");
   1284         fs::write(&file_path, "data").expect("file");
   1285         fs::create_dir(&dir_path).expect("dir");
   1286 
   1287         remove_path_if_exists(&file_path).expect("remove file");
   1288         remove_path_if_exists(&dir_path).expect("remove dir");
   1289         remove_path_if_exists(dir.path().join("missing").as_path()).expect("remove missing");
   1290 
   1291         assert!(!file_path.exists());
   1292         assert!(!dir_path.exists());
   1293     }
   1294 
   1295     #[test]
   1296     fn remove_path_from_state_reports_dir_file_and_metadata_errors() {
   1297         let dir = tempdir().expect("tempdir");
   1298         let dir_path = dir.path().join("subdir");
   1299         let file_path = dir.path().join("file.txt");
   1300         let metadata_path = dir.path().join("metadata");
   1301         ok_remove_path(Path::new("/")).expect("noop remove path");
   1302 
   1303         let dir_err = remove_path_from_state(
   1304             &dir_path,
   1305             Ok(Some(ExistingPathKind::Directory)),
   1306             deny_remove_path,
   1307             ok_remove_path,
   1308         )
   1309         .expect_err("directory removal should fail");
   1310         assert_error_contains(
   1311             &dir_err,
   1312             &[dir_path.to_string_lossy().as_ref(), "remove managed path"],
   1313         );
   1314 
   1315         let file_err = remove_path_from_state(
   1316             &file_path,
   1317             Ok(Some(ExistingPathKind::File)),
   1318             ok_remove_path,
   1319             deny_remove_path,
   1320         )
   1321         .expect_err("file removal should fail");
   1322         assert_error_contains(
   1323             &file_err,
   1324             &[file_path.to_string_lossy().as_ref(), "remove managed path"],
   1325         );
   1326 
   1327         let metadata_err = remove_path_from_state(
   1328             &metadata_path,
   1329             Err(io::Error::new(
   1330                 io::ErrorKind::PermissionDenied,
   1331                 "metadata lookup failed",
   1332             )),
   1333             ok_remove_path,
   1334             ok_remove_path,
   1335         )
   1336         .expect_err("metadata lookup should fail");
   1337         assert_error_contains(
   1338             &metadata_err,
   1339             &[
   1340                 metadata_path.to_string_lossy().as_ref(),
   1341                 "read managed file",
   1342             ],
   1343         );
   1344     }
   1345 
   1346     #[test]
   1347     fn remove_pid_file_reports_directory_errors() {
   1348         let dir = tempdir().expect("tempdir");
   1349         let mut paths = sample_paths(dir.path());
   1350         ensure_instance_layout(&paths).expect("layout");
   1351         paths.pid_file_path = paths.run_dir.clone();
   1352 
   1353         let err = super::remove_pid_file(&paths).expect_err("directory pid path should fail");
   1354         assert_error_contains(
   1355             &err,
   1356             &[
   1357                 paths.run_dir.to_string_lossy().as_ref(),
   1358                 "remove managed path",
   1359             ],
   1360         );
   1361     }
   1362 
   1363     #[test]
   1364     fn remove_pid_file_accepts_missing_pid_paths() {
   1365         let dir = tempdir().expect("tempdir");
   1366         let paths = sample_paths(dir.path());
   1367         super::remove_pid_file(&paths).expect("missing pid file should be ignored");
   1368     }
   1369 
   1370     #[cfg(unix)]
   1371     #[test]
   1372     fn set_mode_helpers_report_missing_path_errors() {
   1373         let dir = tempdir().expect("tempdir");
   1374         let missing = dir.path().join("missing");
   1375 
   1376         let err = set_executable_mode(&missing).expect_err("missing executable should fail");
   1377         assert_error_contains(
   1378             &err,
   1379             &[missing.to_string_lossy().as_ref(), "read managed file"],
   1380         );
   1381 
   1382         let err = set_secret_mode(&missing).expect_err("missing secret should fail");
   1383         assert_error_contains(
   1384             &err,
   1385             &[missing.to_string_lossy().as_ref(), "read managed file"],
   1386         );
   1387     }
   1388 
   1389     #[cfg(unix)]
   1390     #[test]
   1391     fn apply_mode_reports_set_permissions_errors() {
   1392         let dir = tempdir().expect("tempdir");
   1393         let path = dir.path().join("radrootsd");
   1394         fs::write(&path, "binary").expect("binary");
   1395 
   1396         let err = apply_mode(&path, 0o755, |_path, _permissions| {
   1397             Err(io::Error::new(
   1398                 io::ErrorKind::PermissionDenied,
   1399                 "set permissions failed",
   1400             ))
   1401         })
   1402         .expect_err("set permissions should fail");
   1403         assert_error_contains(&err, &[path.to_string_lossy().as_ref(), "set permissions"]);
   1404     }
   1405 
   1406     #[cfg(unix)]
   1407     #[test]
   1408     fn remove_path_if_exists_reports_metadata_errors() {
   1409         let dir = tempdir().expect("tempdir");
   1410         let restricted = dir.path().join("restricted");
   1411         fs::create_dir(&restricted).expect("restricted dir");
   1412         let blocked_path = restricted.join("child");
   1413 
   1414         let mut permissions = fs::metadata(&restricted).expect("metadata").permissions();
   1415         permissions.set_mode(0);
   1416         fs::set_permissions(&restricted, permissions).expect("restrict permissions");
   1417 
   1418         let err = remove_path_if_exists(&blocked_path).expect_err("metadata lookup should fail");
   1419 
   1420         let mut restore = fs::metadata(&restricted)
   1421             .expect("restricted metadata")
   1422             .permissions();
   1423         restore.set_mode(0o755);
   1424         fs::set_permissions(&restricted, restore).expect("restore permissions");
   1425 
   1426         assert_error_contains(
   1427             &err,
   1428             &[blocked_path.to_string_lossy().as_ref(), "read managed file"],
   1429         );
   1430     }
   1431 
   1432     #[cfg(unix)]
   1433     #[test]
   1434     fn signal_helpers_cover_failure_paths() {
   1435         let missing_pid = 999_999_u32;
   1436         assert!(!process_running_for_pid(missing_pid));
   1437 
   1438         let err = terminate_process(missing_pid).expect_err("terminate should fail");
   1439         assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
   1440 
   1441         let err = force_kill_process(missing_pid).expect_err("force kill should fail");
   1442         assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
   1443 
   1444         let err = signal_process(missing_pid, "-BOGUS").expect_err("invalid signal should fail");
   1445         assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
   1446     }
   1447 
   1448     #[cfg(unix)]
   1449     #[test]
   1450     fn signal_process_with_reports_execution_errors() {
   1451         let err = signal_process_with(42, "-TERM", |_pid, _signal| {
   1452             Err(io::Error::new(
   1453                 io::ErrorKind::NotFound,
   1454                 "kill executable missing",
   1455             ))
   1456         })
   1457         .expect_err("signal execution should fail");
   1458         assert_error_contains(&err, &["42", "-TERM", "kill executable missing"]);
   1459     }
   1460 
   1461     #[cfg(unix)]
   1462     #[test]
   1463     fn process_running_state_from_ps_output_handles_non_success_and_zombies() {
   1464         assert!(process_running_state_from_ps_output(output_with_status(
   1465             exit_status(1),
   1466             b"",
   1467         )));
   1468         assert!(!process_running_state_from_ps_output(output_with_status(
   1469             exit_status(0),
   1470             b"Z+",
   1471         )));
   1472         assert!(process_running_state_from_ps_output(output_with_status(
   1473             exit_status(0),
   1474             b"S+",
   1475         )));
   1476     }
   1477 }