job_object.rs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. //! Windows Job Object for reliable child process cleanup.
  2. //!
  3. //! This module provides a wrapper around Windows Job Objects with the
  4. //! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` flag set. When the job object handle
  5. //! is closed (including when the parent process exits or crashes), Windows
  6. //! automatically terminates all processes assigned to the job.
  7. //!
  8. //! This is more reliable than manual cleanup because it works even if:
  9. //! - The parent process crashes
  10. //! - The parent is killed via Task Manager
  11. //! - The RunEvent::Exit handler fails to run
  12. use std::io::{Error, Result};
  13. #[cfg(windows)]
  14. use std::sync::Mutex;
  15. use windows::Win32::Foundation::{CloseHandle, HANDLE};
  16. use windows::Win32::System::JobObjects::{
  17. AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
  18. JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
  19. SetInformationJobObject,
  20. };
  21. use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
  22. /// A Windows Job Object configured to kill all assigned processes when closed.
  23. ///
  24. /// When this struct is dropped or when the owning process exits (even abnormally),
  25. /// Windows will automatically terminate all processes that have been assigned to it.
  26. pub struct JobObject(HANDLE);
  27. // SAFETY: HANDLE is just a pointer-sized value, and Windows job objects
  28. // can be safely accessed from multiple threads.
  29. unsafe impl Send for JobObject {}
  30. unsafe impl Sync for JobObject {}
  31. impl JobObject {
  32. /// Creates a new anonymous job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set.
  33. ///
  34. /// When the last handle to this job is closed (including on process exit),
  35. /// Windows will terminate all processes assigned to the job.
  36. pub fn new() -> Result<Self> {
  37. unsafe {
  38. // Create an anonymous job object
  39. let job = CreateJobObjectW(None, None).map_err(|e| Error::other(e.message()))?;
  40. // Configure the job to kill all processes when the handle is closed
  41. let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
  42. info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
  43. SetInformationJobObject(
  44. job,
  45. JobObjectExtendedLimitInformation,
  46. &info as *const _ as *const std::ffi::c_void,
  47. std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
  48. )
  49. .map_err(|e| Error::other(e.message()))?;
  50. Ok(Self(job))
  51. }
  52. }
  53. /// Assigns a process to this job object by its process ID.
  54. ///
  55. /// Once assigned, the process will be terminated when this job object is dropped
  56. /// or when the owning process exits.
  57. ///
  58. /// # Arguments
  59. /// * `pid` - The process ID of the process to assign
  60. pub fn assign_pid(&self, pid: u32) -> Result<()> {
  61. unsafe {
  62. // Open a handle to the process with the minimum required permissions
  63. // PROCESS_SET_QUOTA and PROCESS_TERMINATE are required by AssignProcessToJobObject
  64. let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
  65. .map_err(|e| Error::other(e.message()))?;
  66. // Assign the process to the job
  67. let result = AssignProcessToJobObject(self.0, process);
  68. // Close our handle to the process - the job object maintains its own reference
  69. let _ = CloseHandle(process);
  70. result.map_err(|e| Error::other(e.message()))
  71. }
  72. }
  73. }
  74. impl Drop for JobObject {
  75. fn drop(&mut self) {
  76. unsafe {
  77. // When this handle is closed and it's the last handle to the job,
  78. // Windows will terminate all processes in the job due to KILL_ON_JOB_CLOSE
  79. let _ = CloseHandle(self.0);
  80. }
  81. }
  82. }
  83. /// Holds the Windows Job Object that ensures child processes are killed when the app exits.
  84. /// On Windows, when the job object handle is closed (including on crash), all assigned
  85. /// processes are automatically terminated by the OS.
  86. #[cfg(windows)]
  87. pub struct JobObjectState {
  88. job: Mutex<Option<JobObject>>,
  89. error: Mutex<Option<String>>,
  90. }
  91. #[cfg(windows)]
  92. impl JobObjectState {
  93. pub fn new() -> Self {
  94. match JobObject::new() {
  95. Ok(job) => Self {
  96. job: Mutex::new(Some(job)),
  97. error: Mutex::new(None),
  98. },
  99. Err(e) => {
  100. eprintln!("Failed to create job object: {e}");
  101. Self {
  102. job: Mutex::new(None),
  103. error: Mutex::new(Some(format!("Failed to create job object: {e}"))),
  104. }
  105. }
  106. }
  107. }
  108. pub fn assign_pid(&self, pid: u32) {
  109. if let Some(job) = self.job.lock().unwrap().as_ref() {
  110. if let Err(e) = job.assign_pid(pid) {
  111. eprintln!("Failed to assign process {pid} to job object: {e}");
  112. *self.error.lock().unwrap() =
  113. Some(format!("Failed to assign process to job object: {e}"));
  114. } else {
  115. println!("Assigned process {pid} to job object for automatic cleanup");
  116. }
  117. }
  118. }
  119. }
  120. #[cfg(test)]
  121. mod tests {
  122. use super::*;
  123. #[test]
  124. fn test_job_object_creation() {
  125. let job = JobObject::new();
  126. assert!(job.is_ok(), "Failed to create job object: {:?}", job.err());
  127. }
  128. }