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 }