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}