Skip to main content

corundum_framework_package/
console.rs

1//! A console shim for a Windows GUI binary.
2//!
3//! An app's GUI binary is built for the Windows GUI subsystem and has no console, so a typed
4//! command prints nothing and returns at once. A tiny console binary on `PATH` calls
5//! [`forward`]: it starts the GUI binary with the same arguments and standard streams, waits,
6//! and exits with its exit code.
7//!
8//! On Windows the child is put in a job object that kills it when the shim goes away, so
9//! Ctrl+C in the console takes the GUI process with it. Only the child: the job lets the
10//! processes it starts leave silently (`JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK`). Velopack
11//! starts its `Update.exe` without asking to leave the job, and the GUI exits for the
12//! update to be applied; in the shim's job, `Update.exe` would be killed with it halfway
13//! through replacing the files, and so would the app it restarts and any daemon.
14//!
15//! A shim has no logging set up, so a job that cannot be made is reported on standard
16//! error.
17//!
18//! A binary without the shim (installed with `cargo install`, or run before there is a
19//! package) calls `corundum_framework_platform::console::attach_parent` instead, on its
20//! command-line starts only (not the one that opens the window): its output then reaches the terminal it was started from, but the shell does
21//! not wait for it, so its exit code and typed input are lost. The shim stays the full
22//! command line.
23
24use std::ffi::OsString;
25use std::path::Path;
26use std::process::Command;
27
28/// Runs the GUI binary `gui_exe` that sits beside this executable with this process's
29/// arguments, and exits with its exit code.
30///
31/// `gui_exe` is the GUI executable's file name as packaged: on Windows the cargo name kept,
32/// `<app>-desktop.exe`, since only macOS and Linux rename it to `<app>`.
33#[cfg(windows)]
34pub fn forward(gui_exe: &str) -> ! {
35    let exe = match std::env::current_exe() {
36        Ok(me) => me.with_file_name(gui_exe),
37        Err(_) => Path::new(gui_exe).to_path_buf(),
38    };
39    std::process::exit(forward_to(&exe, std::env::args_os().skip(1)))
40}
41
42/// Runs `exe` with `args` and this process's standard streams, waits, and returns its exit
43/// code (1 when it ended without one or could not start).
44///
45/// On Windows the child joins a job object that kills it when the shim goes away, so Ctrl+C
46/// in the console takes the GUI process with it.
47pub fn forward_to(exe: &Path, args: impl IntoIterator<Item = OsString>) -> i32 {
48    let mut child = match Command::new(exe).args(args).spawn() {
49        Ok(child) => child,
50        Err(e) => {
51            eprintln!("cannot start {}: {e}", exe.display());
52            return 1;
53        }
54    };
55    #[cfg(windows)]
56    let _job = job::kill_on_close(&child);
57    match child.wait() {
58        Ok(status) => status.code().unwrap_or(1),
59        Err(e) => {
60            eprintln!("cannot wait for {}: {e}", exe.display());
61            1
62        }
63    }
64}
65
66#[cfg(windows)]
67mod job {
68    use std::os::windows::io::AsRawHandle;
69    use std::process::Child;
70
71    use windows::Win32::Foundation::{CloseHandle, HANDLE};
72    use windows::Win32::System::JobObjects::{
73        AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
74        JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
75        JobObjectExtendedLimitInformation, SetInformationJobObject,
76    };
77
78    /// A job object handle; closing it (when the shim ends, however it ends) kills the child.
79    pub struct Job(HANDLE);
80
81    impl Drop for Job {
82        fn drop(&mut self) {
83            // SAFETY: the handle came from `CreateJobObjectW` and is closed only here.
84            let _ = unsafe { CloseHandle(self.0) };
85        }
86    }
87
88    /// Puts `child` in a new job that kills it on close. Without one (a failure is only
89    /// printed: the shim has no logging) the child still runs; it just outlives a Ctrl+C.
90    pub fn kill_on_close(child: &Child) -> Option<Job> {
91        match try_kill_on_close(child) {
92            Ok(job) => Some(job),
93            Err(e) => {
94                eprintln!("the console shim runs without a job object: {e}");
95                None
96            }
97        }
98    }
99
100    fn try_kill_on_close(child: &Child) -> windows::core::Result<Job> {
101        // SAFETY: plain Win32 calls on handles that are valid for their duration; the info
102        // struct outlives the call and its size is passed along.
103        unsafe {
104            let job = Job(CreateJobObjectW(None, None)?);
105            let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
106            // Only the child is killed: what it starts (Velopack's `Update.exe`, which must
107            // outlive the GUI to apply an update) leaves the job.
108            info.BasicLimitInformation.LimitFlags =
109                JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
110            SetInformationJobObject(
111                job.0,
112                JobObjectExtendedLimitInformation,
113                &info as *const _ as *const _,
114                std::mem::size_of_val(&info) as u32,
115            )?;
116            AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle()))?;
117            Ok(job)
118        }
119    }
120}