fix: restore Ctrl+C handling for Windows terminal children
This commit is contained in:
1 parent
75dc0b9481
commit
80cf48943d
3 files changed
+136
No files matched your search
@@ -223,6 +223,14 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if options.subcommands.is_none()
|
||||
&& let Err(error) = platform::startup::prepare_console_for_gui()
|
||||
{
|
||||
platform::startup::report_error(&error, true);
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
// 产品主窗:GPUI 作为 nebula.exe 的 UI 层,从主线程直接进 GPUI
|
||||
// 消息循环,winit 旧壳完全不启动。1.1.0 安装包 / 双击 / 资源管理器
|
||||
// 右键都走这里;`--legacy-shell` 才回旧壳。
|
||||
|
||||
@@ -6,6 +6,21 @@ pub(crate) fn report_error(error: &dyn std::fmt::Display, gui_launch: bool) {
|
||||
let _ = (error, gui_launch);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn prepare_console_for_gui() -> std::io::Result<()> {
|
||||
// The NULL-handler ignore flag is inherited by ConPTY shells. Normalize it
|
||||
// once, before any workers or terminals exist; never toggle it around spawn.
|
||||
// SAFETY: this changes only this process's console attribute, not its parent
|
||||
// or any existing child. The call also works without an attached console.
|
||||
if unsafe { windows_sys::Win32::System::Console::SetConsoleCtrlHandler(None, 0) } == 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
mod console_tests;
|
||||
|
||||
pub fn prepare_gui() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Console attributes are process-global: each scenario runs in a disposable process.
|
||||
use std::env;
|
||||
use std::process::{Child, Command};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use windows_sys::Win32::System::Console::{
|
||||
AllocConsole, CTRL_C_EVENT, FreeConsole, GenerateConsoleCtrlEvent, SetConsoleCtrlHandler,
|
||||
};
|
||||
|
||||
const TEST: &str = "platform::startup::console_tests::gui_startup_restores_child_ctrl_c";
|
||||
|
||||
struct TestChild(Child);
|
||||
|
||||
impl Drop for TestChild {
|
||||
fn drop(&mut self) {
|
||||
// Failure cleanup only: a killed child never counts as an interruption.
|
||||
if !matches!(self.0.try_wait(), Ok(Some(_))) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadyFile(std::path::PathBuf);
|
||||
|
||||
impl Drop for ReadyFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn wait(child: &mut TestChild) -> std::process::ExitStatus {
|
||||
let deadline = Instant::now() + Duration::from_secs(12);
|
||||
loop {
|
||||
if let Some(status) = child.0.try_wait().unwrap() {
|
||||
return status;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "console test child exceeded its deadline");
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_startup_restores_child_ctrl_c() {
|
||||
let Ok(role) = env::var("PEBREL_CONSOLE_TEST_ROLE") else {
|
||||
for detached in [false, true] {
|
||||
let mut child = TestChild(
|
||||
Command::new(env::current_exe().unwrap())
|
||||
.args(["--exact", TEST, "--nocapture"])
|
||||
.env("PEBREL_CONSOLE_TEST_ROLE", "host")
|
||||
.env("PEBREL_CONSOLE_TEST_DETACHED", detached.to_string())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
assert!(wait(&mut child).success(), "GUI console startup failed (detached={detached})");
|
||||
}
|
||||
return;
|
||||
};
|
||||
if role == "target" {
|
||||
// The product uses the GUI subsystem, so its test executable must
|
||||
// attach explicitly just as main does before it can receive events.
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Console::AttachConsole(
|
||||
windows_sys::Win32::System::Console::ATTACH_PARENT_PROCESS,
|
||||
);
|
||||
}
|
||||
let ready = env::var_os("PEBREL_CONSOLE_TEST_READY").unwrap();
|
||||
std::fs::write(ready, b"ready").unwrap();
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
return;
|
||||
}
|
||||
|
||||
// Never broadcast to cargo's or the developer's console.
|
||||
unsafe { FreeConsole() };
|
||||
assert_ne!(unsafe { AllocConsole() }, 0);
|
||||
assert_ne!(unsafe { SetConsoleCtrlHandler(None, 1) }, 0);
|
||||
let detached = env::var("PEBREL_CONSOLE_TEST_DETACHED").unwrap() == "true";
|
||||
if detached {
|
||||
assert_ne!(unsafe { FreeConsole() }, 0);
|
||||
}
|
||||
super::prepare_console_for_gui().unwrap();
|
||||
if detached {
|
||||
// The API must also succeed for Explorer/detached launches.
|
||||
return;
|
||||
}
|
||||
let stamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let ready_file = ReadyFile(
|
||||
env::temp_dir().join(format!("pebrel-ctrl-ready-{}-{stamp}", std::process::id())),
|
||||
);
|
||||
let ready = &ready_file.0;
|
||||
let mut target = TestChild(
|
||||
Command::new(env::current_exe().unwrap())
|
||||
.args(["--exact", TEST, "--nocapture"])
|
||||
.env("PEBREL_CONSOLE_TEST_ROLE", "target")
|
||||
.env("PEBREL_CONSOLE_TEST_READY", &ready)
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(3);
|
||||
while !ready.exists() {
|
||||
assert!(Instant::now() < deadline, "target did not become ready");
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
// Shield only the disposable test driver AFTER the child inherited the
|
||||
// production startup state. This cannot change the existing child's state.
|
||||
assert_ne!(unsafe { SetConsoleCtrlHandler(None, 1) }, 0);
|
||||
let started = Instant::now();
|
||||
assert_ne!(unsafe { GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0) }, 0);
|
||||
let status = wait(&mut target);
|
||||
assert_eq!(status.code().map(|code| code as u32), Some(0xc000013a));
|
||||
assert!(started.elapsed() < Duration::from_secs(3), "child did not interrupt promptly");
|
||||
}
|
||||
Reference in new issue
Block a user