Skip to main content

magi/
proc.rs

1//! Spawning child processes without putting a window on the operator's screen.
2//!
3//! Every external program magi runs - the agent CLIs, `git`, `gh`, the
4//! configured verification commands - is a console application. What happens
5//! when one is spawned depends on whether the *parent* has a console, and
6//! magi has two kinds of parent:
7//!
8//! - `magi run` / `magi review` in a terminal. The child inherits that
9//!   console, writes nowhere visible because its pipes are redirected, and
10//!   nothing appears.
11//! - `magi web`, which serves the deck. Its successor is spawned
12//!   `DETACHED_PROCESS` on purpose (see [`crate::web`]): it has to outlive the
13//!   process that started it and must not hold a pipe a terminal is waiting
14//!   on. **That process has no console at all**, so Windows allocates a brand
15//!   new one for each console child - and draws it. An implement wave is
16//!   three agents, so three black windows opened over whatever the operator
17//!   was doing, in front of the browser they were reading the deck in.
18//!
19//! `CREATE_NO_WINDOW` is the answer to exactly that: the child still gets a
20//! console for its standard handles, and that console is never shown. It is
21//! not the same as `DETACHED_PROCESS`, which gives the child no console and
22//! would make a grandchild pop a window of its own for the same reason.
23//!
24//! Nothing here is conditional on how magi was started. A hidden console is
25//! correct in a terminal too: the pipes are redirected either way, so there
26//! was never anything to look at.
27
28/// `CREATE_NO_WINDOW` - run the child's console, but never draw it.
29///
30/// From `processthreadsapi.h`. Spelled out rather than pulled in from a
31/// bindings crate: it is one number that has been stable since Windows 2000,
32/// and the alternative is a dependency for it.
33#[cfg(windows)]
34const CREATE_NO_WINDOW: u32 = 0x0800_0000;
35
36/// Spawn without a visible console window.
37///
38/// Implemented for both `Command` types magi uses - `std` for the few
39/// synchronous calls, `tokio` for everything else - so a call site does not
40/// have to know which one it is holding, and so no call site has to repeat a
41/// `#[cfg(windows)]` block to get it.
42///
43/// A no-op off Windows, where a spawned process has no window to begin with.
44pub trait Quiet {
45    /// Apply it, and hand the command back for further building.
46    fn quiet(&mut self) -> &mut Self;
47}
48
49impl Quiet for std::process::Command {
50    fn quiet(&mut self) -> &mut Self {
51        #[cfg(windows)]
52        {
53            use std::os::windows::process::CommandExt as _;
54            self.creation_flags(CREATE_NO_WINDOW);
55        }
56        self
57    }
58}
59
60impl Quiet for tokio::process::Command {
61    fn quiet(&mut self) -> &mut Self {
62        #[cfg(windows)]
63        {
64            self.creation_flags(CREATE_NO_WINDOW);
65        }
66        self
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    /// The flag is the one Windows documents, and not one of the two it is
75    /// easily confused with.
76    ///
77    /// `DETACHED_PROCESS` (0x8) is what leaves a process without a console -
78    /// which is what caused the windows this module exists to stop, because a
79    /// child of such a process gets a fresh console *with* a window.
80    /// `CREATE_NEW_CONSOLE` (0x10) asks for the window outright.
81    #[cfg(windows)]
82    #[test]
83    fn the_flag_hides_a_console_rather_than_removing_or_creating_one() {
84        assert_eq!(CREATE_NO_WINDOW, 0x0800_0000);
85        assert_ne!(CREATE_NO_WINDOW, 0x0000_0008, "DETACHED_PROCESS");
86        assert_ne!(CREATE_NO_WINDOW, 0x0000_0010, "CREATE_NEW_CONSOLE");
87    }
88
89    /// Applying it does not disturb the command being built.
90    ///
91    /// The trait returns `&mut Self` so it can sit in the middle of a builder
92    /// chain, and a call site that put it there must not lose its program or
93    /// arguments to it.
94    #[test]
95    fn quiet_leaves_the_command_it_was_handed_intact() {
96        let mut cmd = tokio::process::Command::new("git");
97        cmd.args(["status", "--short"]).quiet();
98        let built = cmd.as_std();
99        assert_eq!(built.get_program(), "git");
100        let args: Vec<_> = built.get_args().collect();
101        assert_eq!(args, ["status", "--short"]);
102    }
103}