Skip to main content

windows_spawn/
options.rs

1//! Per-spawn capabilities and creation policy.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::ops::{BitOr, BitOrAssign};
6
7use windows_sys::Win32::System::Threading::{
8    CREATE_BREAKAWAY_FROM_JOB, CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE,
9    CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CREATE_PRESERVE_CODE_AUTHZ_LEVEL, DETACHED_PROCESS,
10    INHERIT_PARENT_AFFINITY,
11};
12
13use crate::handles::{AsPseudoConsole, Job, ParentProcess};
14use crate::mitigation::MitigationPolicy;
15
16/// What dropping a live [`crate::Child`] does to its process tree.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum DropPolicy {
19    /// Close windows-spawn's process handle without terminating the process.
20    #[default]
21    Detach,
22    /// Terminate the child and all descendants in windows-spawn's private Job.
23    KillTree,
24}
25
26/// Safe, named `CreateProcessW` creation flags.
27///
28/// Unicode-environment, extended-startup-info, and suspended flags are set
29/// internally. There is no raw-bits constructor.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
31pub struct CreationFlags(u32);
32
33impl CreationFlags {
34    /// Creates an empty flag set.
35    #[must_use]
36    pub const fn new() -> Self {
37        Self(0)
38    }
39
40    /// Creates a process without inheriting a console.
41    pub const DETACHED_PROCESS: Self = Self(DETACHED_PROCESS);
42    /// Gives the child a new console.
43    pub const NEW_CONSOLE: Self = Self(CREATE_NEW_CONSOLE);
44    /// Makes the child the root of a new process group.
45    pub const NEW_PROCESS_GROUP: Self = Self(CREATE_NEW_PROCESS_GROUP);
46    /// Inherits the parent's processor affinity.
47    pub const INHERIT_PARENT_AFFINITY: Self = Self(INHERIT_PARENT_AFFINITY);
48    /// Allows the child to break away from the caller's Job when permitted.
49    pub const BREAKAWAY_FROM_JOB: Self = Self(CREATE_BREAKAWAY_FROM_JOB);
50    /// Preserves the caller's code-authorization level.
51    pub const PRESERVE_CODE_AUTHZ_LEVEL: Self = Self(CREATE_PRESERVE_CODE_AUTHZ_LEVEL);
52    /// Prevents the child from inheriting the caller's hard-error mode.
53    pub const DEFAULT_ERROR_MODE: Self = Self(CREATE_DEFAULT_ERROR_MODE);
54    /// Runs a console application without creating a console window.
55    pub const NO_WINDOW: Self = Self(CREATE_NO_WINDOW);
56
57    pub(crate) const fn bits(self) -> u32 {
58        self.0
59    }
60
61    pub(crate) const fn contains(self, other: Self) -> bool {
62        self.0 & other.0 == other.0
63    }
64}
65
66struct PseudoConsole<'a> {
67    raw: isize,
68    _borrow: PhantomData<&'a dyn AsPseudoConsole>,
69}
70
71impl<'a> PseudoConsole<'a> {
72    fn new<T: AsPseudoConsole>(pseudoconsole: &'a T) -> Self {
73        Self {
74            raw: pseudoconsole.raw_pseudoconsole(),
75            _borrow: PhantomData,
76        }
77    }
78}
79
80impl BitOr for CreationFlags {
81    type Output = Self;
82
83    fn bitor(self, rhs: Self) -> Self::Output {
84        Self(self.0 | rhs.0)
85    }
86}
87
88impl BitOrAssign for CreationFlags {
89    fn bitor_assign(&mut self, rhs: Self) {
90        self.0 |= rhs.0;
91    }
92}
93
94/// Capabilities and policy needed only for one spawn operation.
95///
96/// Jobs are retained in call order, from the root Job to the innermost Job.
97/// The options borrow every capability; they never assume ownership of a Job,
98/// parent process, or pseudoconsole.
99///
100/// The borrow cannot escape the capability it protects:
101///
102/// ```compile_fail
103/// use windows_spawn::{Command, Job, SpawnOptions};
104///
105/// let options;
106/// {
107///     let job = Job::create().unwrap();
108///     options = SpawnOptions::new().job(&job);
109/// }
110/// Command::new("cmd.exe").spawn_with(options).unwrap();
111/// ```
112pub struct SpawnOptions<'a> {
113    pub(crate) jobs: Vec<&'a Job>,
114    pub(crate) parent: Option<&'a ParentProcess>,
115    pub(crate) mitigation: MitigationPolicy,
116    pseudoconsole: Option<PseudoConsole<'a>>,
117    pub(crate) creation_flags: CreationFlags,
118    pub(crate) drop_policy: DropPolicy,
119}
120
121impl fmt::Debug for SpawnOptions<'_> {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        formatter
124            .debug_struct("SpawnOptions")
125            .field("jobs", &self.jobs)
126            .field("parent", &self.parent)
127            .field("mitigation", &self.mitigation)
128            .field(
129                "pseudoconsole",
130                &self.pseudoconsole.as_ref().map(|_| "borrowed"),
131            )
132            .field("creation_flags", &self.creation_flags)
133            .field("drop_policy", &self.drop_policy)
134            .finish()
135    }
136}
137
138impl Default for SpawnOptions<'_> {
139    fn default() -> Self {
140        Self {
141            jobs: Vec::new(),
142            parent: None,
143            mitigation: MitigationPolicy::new(),
144            pseudoconsole: None,
145            creation_flags: CreationFlags::new(),
146            drop_policy: DropPolicy::Detach,
147        }
148    }
149}
150
151impl<'a> SpawnOptions<'a> {
152    /// Creates default per-spawn options.
153    #[must_use]
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Appends a Job, preserving root-to-inner ordering.
159    #[must_use]
160    pub fn job(mut self, job: &'a Job) -> Self {
161        self.jobs.push(job);
162        self
163    }
164
165    /// Chooses another process as the logical parent.
166    #[must_use]
167    pub fn parent_process(mut self, parent: &'a ParentProcess) -> Self {
168        self.parent = Some(parent);
169        self
170    }
171
172    /// Applies a process-creation mitigation policy.
173    #[must_use]
174    pub const fn mitigation(mut self, mitigation: MitigationPolicy) -> Self {
175        self.mitigation = mitigation;
176        self
177    }
178
179    /// Attaches the child to a borrowed pseudoconsole.
180    #[must_use]
181    pub fn pseudoconsole<T: AsPseudoConsole>(mut self, pseudoconsole: &'a T) -> Self {
182        self.pseudoconsole = Some(PseudoConsole::new(pseudoconsole));
183        self
184    }
185
186    /// Adds named process-creation flags.
187    #[must_use]
188    pub const fn creation_flags(mut self, flags: CreationFlags) -> Self {
189        self.creation_flags = flags;
190        self
191    }
192
193    /// Chooses the live-child drop policy.
194    #[must_use]
195    pub const fn drop_policy(mut self, policy: DropPolicy) -> Self {
196        self.drop_policy = policy;
197        self
198    }
199
200    pub(crate) fn pseudoconsole_raw(&self) -> Option<isize> {
201        self.pseudoconsole
202            .as_ref()
203            .map(|pseudoconsole| pseudoconsole.raw)
204    }
205}
206
207#[cfg(test)]
208#[allow(unsafe_code)]
209mod tests {
210    use std::cell::Cell;
211
212    use super::*;
213
214    struct TestPseudoConsole(Cell<isize>);
215
216    // SAFETY: tests use the value only as a snapshot and never pass it to Win32.
217    unsafe impl AsPseudoConsole for TestPseudoConsole {
218        fn raw_pseudoconsole(&self) -> isize {
219            self.0.get()
220        }
221    }
222
223    #[test]
224    fn creation_flags_combine_idempotently_and_options_keep_job_order() {
225        let combined = CreationFlags::NEW_PROCESS_GROUP | CreationFlags::DEFAULT_ERROR_MODE;
226        assert_eq!(combined.bits(), 0x0400_0200);
227        assert_eq!(
228            (CreationFlags::NEW_PROCESS_GROUP | CreationFlags::NEW_PROCESS_GROUP).bits(),
229            CreationFlags::NEW_PROCESS_GROUP.bits()
230        );
231        let mut assigned = CreationFlags::NEW_PROCESS_GROUP;
232        assigned |= CreationFlags::DEFAULT_ERROR_MODE;
233        assert_eq!(assigned.bits(), combined.bits());
234
235        let outer = Job::create().unwrap();
236        let inner = Job::create().unwrap();
237        let options = SpawnOptions::new().job(&outer).job(&inner);
238        assert_eq!(options.jobs.len(), 2);
239        assert!(std::ptr::eq(options.jobs[0], &outer));
240        assert!(std::ptr::eq(options.jobs[1], &inner));
241    }
242
243    #[test]
244    fn pseudoconsole_builder_snapshots_the_raw_value() {
245        let pseudoconsole = TestPseudoConsole(Cell::new(42));
246        let options = SpawnOptions::new().pseudoconsole(&pseudoconsole);
247        pseudoconsole.0.set(99);
248        assert_eq!(options.pseudoconsole_raw(), Some(42));
249        assert!(format!("{options:?}").contains("borrowed"));
250    }
251}