zng_env/
windows_subsystem.rs

1//! Helpers for apps built with `#![windows_subsystem = "windows"]`.
2//!
3//! The Windows operating system does not support hybrid CLI and GUI apps in the same executable,
4//! this module contains helpers that help provide a *best effort* compatibility, based on the tricks
5//! Microsoft Visual Studio uses.
6//!
7//! The [`attach_console`] function must be called at the start of the hybrid executable when it determinate
8//! it is running in CLI mode, the [`build_cli_com_proxy`] must be called in the build script for the hybrid executable.
9//!
10//! The functions in this module are noop in other systems.
11//!
12//! See the `zng::env::windows_subsystem` docs for a full example.
13
14/// Connect to parent stdio if disconnected.
15///
16/// In a CLI app this does nothing, in a GUI app (windows_subsystem = "windows") attaches to console.
17///
18/// Note that the Windows console returns immediately when it spawns `"windows"` executables, so any output
19/// will not appear to be from your app and nothing stops the user from spawning another app causing text
20/// from your app to mix with other streams. This is bas but it is what VSCode does, see [`build_cli_com_proxy`] for
21/// how to implement a more polished solution.
22///
23/// [`build_cli_com_proxy`]: https://zng-ui.github.io/doc/zng_env/windows_subsystem/fn.build_cli_com_proxy.html
24pub fn attach_console() {
25    imp::attach_console();
26}
27
28/// Compile a small console executable that proxies CLI requests to a full hybrid CLI and GUI executable.
29///
30/// The `exe_name` must be the name of the full executable, with lower case `.exe` extension.
31///
32/// The `exe_dir` is only required if the target dir is not `$OUT_DIR/../../../`.
33///
34/// The full executable must call [`attach_console`] at the beginning.
35///
36/// # How it Works
37///
38/// This will compile a `foo.com` executable beside the `foo.exe`, both must be deployed in the same dir.
39/// When users call the `foo` command from command line the `foo.com` is selected, it simply proxies all requests to the
40/// `foo.exe` and holds the console open. This is the same trick used by Visual Studio with `devenv.com` and `devenv.exe`.
41///
42/// # Code Signing
43///
44/// If you code sign the full executable or configure any other policy metadata on it you mut repeat the signing for the
45/// proxy executable too. Note that the generated .com executable is a normal PE file (.exe), it is just renamed to have higher priority.
46///
47/// # Panics
48///
49/// Panics if not called in a build script (build.rs). Returns an error in case of sporadic IO errors.
50#[cfg(feature = "build_cli_com_proxy")]
51pub fn build_cli_com_proxy(exe_name: &str, exe_dir: Option<std::path::PathBuf>) -> std::io::Result<()> {
52    imp::build_cli_com_proxy(exe_name, exe_dir)
53}
54
55#[cfg(not(windows))]
56mod imp {
57    pub fn attach_console() {}
58    #[cfg(feature = "build_cli_com_proxy")]
59    pub fn build_cli_com_proxy(_: &str, _: Option<std::path::PathBuf>) -> std::io::Result<()> {
60        unreachable!()
61    }
62}
63
64#[cfg(windows)]
65mod imp {
66
67    pub fn attach_console() {
68        #[link(name = "kernel32")]
69        unsafe extern "system" {
70            fn GetConsoleWindow() -> isize;
71            fn AttachConsole(process_id: u32) -> i32;
72        }
73        unsafe {
74            // If no console is attached, attempt to attach to parent
75            if GetConsoleWindow() == 0 {
76                let _ = AttachConsole(0xFFFFFFFF);
77            }
78        }
79    }
80
81    #[cfg(feature = "build_cli_com_proxy")]
82    pub fn build_cli_com_proxy(exe_name: &str, exe_dir: Option<std::path::PathBuf>) -> std::io::Result<()> {
83        use std::{
84            env, fs,
85            path::{Path, PathBuf},
86            process::Command,
87        };
88
89        macro_rules! proxy {
90        ($($code:tt)+) => {
91            #[allow(unused)]
92            mod validate {
93                $($code)*
94            }
95            const CODE: &str = stringify!($($code)*);
96        };
97    }
98        proxy! {
99            #![crate_name = "zng_env_build_cli_com_proxy"]
100            use std::{
101                env,
102                process::{Command, Stdio},
103            };
104            fn main() {
105                let mut exe = Command::new(env::current_exe().unwrap().with_file_name("{EXE_NAME}"))
106                    .args(env::args_os().skip(1))
107                    .stdin(Stdio::inherit())
108                    .stdout(Stdio::inherit())
109                    .stderr(Stdio::inherit())
110                    .spawn()
111                    .unwrap();
112                let status = exe.wait().unwrap();
113                std::process::exit(status.code().unwrap_or(1));
114            }
115        }
116        let code = CODE.replace("{EXE_NAME}", exe_name);
117
118        let name = exe_name.strip_suffix(".exe").expect("expected name with .exe extension");
119        let com_name = format!("{name}.com");
120
121        let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR, must be called in build.rs"));
122        let proxy_src = out_dir.join(format!("zng-env-com-proxy.{name}.rs"));
123        let proxy_com = out_dir.join(&com_name);
124        std::fs::write(&proxy_src, code)?;
125        let status = Command::new("rustc")
126            .arg(&proxy_src)
127            .arg("-o")
128            .arg(&proxy_com)
129            .arg("-C")
130            .arg("opt-level=z")
131            .arg("-C")
132            .arg("panic=abort")
133            .arg("-C")
134            .arg("strip=symbols")
135            .status()?;
136        if !status.success() {
137            panic!("failed to compile generated cli com proxy");
138        }
139
140        let target_dir = match &exe_dir {
141            Some(d) => d.as_path(),
142            None => {
143                let d = || -> Option<&Path> { out_dir.parent()?.parent()?.parent() };
144                d().expect("cannot find exe_dir")
145            }
146        };
147        let final_proxy_com = target_dir.join(&com_name);
148        fs::copy(&proxy_com, &final_proxy_com)?;
149
150        Ok(())
151    }
152}