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}