Windows: Tesseract and the --control tools start without a console window (#6735)

This commit is contained in:
ljkx authored and GitHub committed 2026-11-10 14:23:38 +07:00
1 parent 06572d57df
commit dec9f3600b
2 files changed
+64 -2

No files matched your search

+32 -1
View File
@@ -343,8 +343,17 @@ impl TesseractCli {
// Known limit: only the direct child is killed at the timeout or on an early cap. A
// wrapper script's children (grandchildren of this process) may survive the kill; the
// cap and the timeout still bound what this run reads and how long it waits.
let mut command = std::process::Command::new(&self.program);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt as _;
// `CREATE_NO_WINDOW`: tesseract is a console program and the app is not, so without
// it Windows opens a console window for every run, which flashes up during OCR. The
// output files above are unaffected.
command.creation_flags(0x0800_0000);
}
let mut child =
std::process::Command::new(&self.program)
command
.args(args)
.stdin(std::process::Stdio::null())
.stdout(out_file)
@@ -894,6 +903,28 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// The program starts without a console window (Windows): the app has no console, so a
/// console program that isn't told otherwise gets a window of its own, which flashes up on
/// every OCR run. PowerShell stands in for tesseract and reports, through the same output file
/// a reading comes back in, its console's window and the processes attached to that console.
/// Both are checked, because a program started without the flag by a process that has a
/// console shares it instead, and that console may itself have no window.
#[cfg(windows)]
#[test]
fn the_program_starts_without_a_console_window() {
const PROBE: &str = r#"Add-Type -Namespace PdfCraftOcrProbe -Name Console -MemberDefinition '[DllImport("kernel32.dll")] public static extern System.IntPtr GetConsoleWindow(); [DllImport("kernel32.dll")] public static extern uint GetConsoleProcessList(uint[] ids, uint count);'
$ids = New-Object uint32[] 256
$n = [PdfCraftOcrProbe.Console]::GetConsoleProcessList($ids, 256)
"$([PdfCraftOcrProbe.Console]::GetConsoleWindow()) $($ids[0..($n-1)] -join ',')""#;
let root = std::env::var_os("SystemRoot").map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);
let t = TesseractCli::new(root.join(r"System32\WindowsPowerShell\v1.0\powershell.exe"));
let text = t.run(&["-NoProfile", "-NonInteractive", "-Command", PROBE], t.timeout, t.max_output).unwrap();
let (window, attached) = text.trim().split_once(' ').unwrap();
assert_eq!(window, "0", "the program has a console window: {text}");
let attached: Vec<u32> = attached.split(',').map(|id| id.parse().unwrap()).collect();
assert!(!attached.is_empty() && !attached.contains(&std::process::id()), "the program shares this process's console: {text}");
}
#[cfg(unix)]
fn make_executable(p: &Path) {
use std::os::unix::fs::PermissionsExt;
+32 -1
View File
@@ -132,10 +132,19 @@ const ICACLS: &str = "icacls.exe";
/// A tool Windows ships, started by its full path (review on #3956). Started by bare name, a
/// `whoami.exe`, `powershell.exe` or `icacls.exe` found first (next to the app, or earlier on the
/// search path) would answer instead, and `whoami`'s answer is what every check here trusts.
///
/// Each tool is a console program and the app is not, so without `CREATE_NO_WINDOW` Windows gives
/// every one of them a console window of its own, which flashes up while the app writes its
/// `--control` file. The flag doesn't touch the pipes `output()` reads the answer through.
#[cfg(windows)]
fn system_tool(relative: &str) -> std::process::Command {
use std::os::windows::process::CommandExt as _;
/// `CREATE_NO_WINDOW`, from the Windows process creation flags.
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let root = std::env::var_os("SystemRoot").map_or_else(|| std::path::PathBuf::from(r"C:\Windows"), std::path::PathBuf::from);
std::process::Command::new(root.join("System32").join(relative))
let mut command = std::process::Command::new(root.join("System32").join(relative));
command.creation_flags(CREATE_NO_WINDOW);
command
}
#[cfg(windows)]
@@ -384,6 +393,28 @@ mod tests {
}
}
/// The tools start without a console window: the app has no console, so a tool that isn't
/// told otherwise gets a window of its own, which flashes up. The started PowerShell reports
/// its console's window and the processes attached to that console. Both are checked, because
/// a tool started without the flag by a process that has a console shares it instead, and that
/// console may itself have no window (a test run started by a service or an IDE).
#[cfg(windows)]
#[test]
fn the_tools_start_without_a_console_window() {
const PROBE: &str = r#"Add-Type -Namespace PdfCraftProbe -Name Console -MemberDefinition '[DllImport("kernel32.dll")] public static extern System.IntPtr GetConsoleWindow(); [DllImport("kernel32.dll")] public static extern uint GetConsoleProcessList(uint[] ids, uint count);'
$ids = New-Object uint32[] 256
$n = [PdfCraftProbe.Console]::GetConsoleProcessList($ids, 256)
"$([PdfCraftProbe.Console]::GetConsoleWindow()) $($ids[0..($n-1)] -join ',')""#;
let out = system_tool(POWERSHELL).args(["-NoProfile", "-NonInteractive", "-Command", PROBE]).output().unwrap();
// The answer still comes back through the pipe.
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "{text} {}", String::from_utf8_lossy(&out.stderr));
let (window, attached) = text.trim().split_once(' ').unwrap();
assert_eq!(window, "0", "the tool has a console window: {text}");
let attached: Vec<u32> = attached.split(',').map(|id| id.parse().unwrap()).collect();
assert!(!attached.is_empty() && !attached.contains(&std::process::id()), "the tool shares this process's console: {text}");
}
#[test]
fn other_owners_readers_and_missing_lists_are_refused() {
let other = "S-1-5-21-1-2-3-1002";