app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
rust: Windows window-control layer (enumerate, process, messages, menu, focus, capture, UIA), all cross-process calls h…
3ecdded
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
//! Process queries and the quiet GUI spawn.
//! Python: win_focus.py `_hwnd_exe_name`, `_pid_exe_base`, `_parent_pid_map`,
//! `background_startupinfo`; close_windows.py `_win_get_kicad_pids`;
//! launch.py's Popen with STARTF_USESHOWWINDOW / SW_SHOWNOACTIVATE and stderr to a log.
use std::os::windows::io::AsRawHandle;
use std::path::Path;
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{
CloseHandle, GetLastError, SetHandleInformation, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, FILETIME, HANDLE,
HANDLE_FLAG_INHERIT,
};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::Threading::{
CreateProcessW, GetExitCodeProcess, GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
CREATE_UNICODE_ENVIRONMENT, DETACHED_PROCESS, PROCESS_INFORMATION, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
PROCESS_TERMINATE, STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES, STARTUPINFOW,
};
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNOACTIVATE;
use super::{from_wide, is_kicad_exe, wide};
use crate::ProcessInfo;
/// Process exit code that means "still running" (STILL_ACTIVE).
const STILL_ACTIVE_CODE: u32 = 259;
/// Owned process handle that closes on drop.
struct Proc(HANDLE);
impl Drop for Proc {
fn drop(&mut self) {
// SAFETY: the handle came from OpenProcess and is closed exactly once.
unsafe {
let _ = CloseHandle(self.0);
}
}
}
fn open_limited(pid: u32) -> Option<Proc> {
// SAFETY: OpenProcess with a query-only right; a failure is an Err, not a bad handle.
unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }.ok().map(Proc)
}
/// Full image path of a process ("" when it cannot be opened, e.g. a protected process).
pub fn exe_path(pid: u32) -> String {
let Some(p) = open_limited(pid) else { return String::new() };
let mut buf = vec![0u16; 1024];
let mut len = buf.len() as u32;
// SAFETY: `len` is the buffer capacity in characters and is updated to the length written.
match unsafe { QueryFullProcessImageNameW(p.0, PROCESS_NAME_WIN32, PWSTR(buf.as_mut_ptr()), &mut len) } {
Ok(()) => from_wide(&buf[..len as usize]),
Err(_) => String::new(),
}
}
/// Executable base name, lower case ("pcbnew.exe"), "" when unknown.
pub fn exe_base_name(pid: u32) -> String {
base_name(&exe_path(pid))
}
fn base_name(path: &str) -> String {
path.rsplit(['\\', '/']).next().unwrap_or("").to_ascii_lowercase()
}
/// Unix epoch seconds when the process started, if the process can be opened.
fn started_at(p: &Proc) -> Option<u64> {
let mut create = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
// SAFETY: four out-params that outlive the call.
unsafe { GetProcessTimes(p.0, &mut create, &mut exit, &mut kernel, &mut user) }.ok()?;
let ft = ((create.dwHighDateTime as u64) << 32) | create.dwLowDateTime as u64;
// FILETIME counts 100 ns ticks since 1601-01-01; 116444736000000000 ticks to 1970.
Some(ft.saturating_sub(116_444_736_000_000_000) / 10_000_000)
}
/// One Toolhelp pass: (pid, parent pid, exe base name from the snapshot) for every process.
pub fn snapshot() -> Vec<(u32, u32, String)> {
let mut out = Vec::with_capacity(256);
// SAFETY: the snapshot handle is closed below on every path.
let Ok(snap) = (unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }) else { return out };
let mut pe = PROCESSENTRY32W { dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32, ..Default::default() };
// SAFETY: dwSize is set; `pe` outlives each call.
if unsafe { Process32FirstW(snap, &mut pe) }.is_ok() {
loop {
out.push((pe.th32ProcessID, pe.th32ParentProcessID, from_wide(&pe.szExeFile).to_ascii_lowercase()));
// SAFETY: as above.
if unsafe { Process32NextW(snap, &mut pe) }.is_err() {
break;
}
}
}
// SAFETY: closing the snapshot handle we own.
unsafe {
let _ = CloseHandle(snap);
}
out
}
/// Running processes whose exe base name is in `exe_names` (any KiCad exe when empty).
/// The Toolhelp snapshot supplies pid, parent and name even for processes we cannot open;
/// the full path and start time need `PROCESS_QUERY_LIMITED_INFORMATION` and are best effort.
pub fn list(exe_names: &[&str]) -> Vec<ProcessInfo> {
let wanted = |exe: &str| {
if exe_names.is_empty() {
is_kicad_exe(exe)
} else {
exe_names.iter().any(|n| n.eq_ignore_ascii_case(exe))
}
};
snapshot()
.into_iter()
.filter(|(pid, _, exe)| *pid != 0 && wanted(exe))
.map(|(pid, ppid, exe)| {
let p = open_limited(pid);
let path = exe_path(pid).replace('\\', "/");
ProcessInfo { pid, exe, path, parent_pid: ppid, started_at: p.as_ref().and_then(started_at) }
})
.collect()
}
/// Is the process alive? `OpenProcess` + `GetExitCodeProcess`; a pid that does not exist
/// fails with ERROR_INVALID_PARAMETER, a protected process with ERROR_ACCESS_DENIED
/// (alive, just not ours to inspect). None only when Windows answers something else.
pub fn alive(pid: u32) -> Option<bool> {
if pid == 0 {
return Some(false);
}
match open_limited(pid) {
Some(p) => {
let mut code = 0u32;
// SAFETY: `code` outlives the call.
match unsafe { GetExitCodeProcess(p.0, &mut code) } {
Ok(()) => Some(code == STILL_ACTIVE_CODE),
Err(_) => None,
}
}
// SAFETY: reading the thread's last error right after the failed OpenProcess.
None => match unsafe { GetLastError() } {
e if e == ERROR_INVALID_PARAMETER => Some(false),
e if e == ERROR_ACCESS_DENIED => Some(true),
_ => None,
},
}
}
pub fn kill(pid: u32) -> Result<(), String> {
// SAFETY: handle closed by Proc's Drop.
let p = unsafe { OpenProcess(PROCESS_TERMINATE, false, pid) }.map(Proc).map_err(|e| format!("OpenProcess({pid}): {e}"))?;
// SAFETY: valid handle with PROCESS_TERMINATE.
unsafe { TerminateProcess(p.0, 1) }.map_err(|e| format!("TerminateProcess({pid}): {e}"))
}
/// Quote one argument the way CreateProcessW's C runtime parsing expects.
pub fn quote_arg(a: &str) -> String {
if !a.is_empty() && !a.chars().any(|c| c == ' ' || c == '\t' || c == '"' || c == '\n') {
return a.to_string();
}
let mut out = String::from("\"");
let mut backslashes = 0;
for c in a.chars() {
match c {
'\\' => backslashes += 1,
'"' => {
out.push_str(&"\\".repeat(backslashes * 2 + 1));
out.push('"');
backslashes = 0;
}
_ => {
out.push_str(&"\\".repeat(backslashes));
out.push(c);
backslashes = 0;
}
}
}
out.push_str(&"\\".repeat(backslashes * 2));
out.push('"');
out
}
/// Our environment plus `extra`, as a CREATE_UNICODE_ENVIRONMENT block.
fn env_block(extra: &[(String, String)]) -> Vec<u16> {
let mut vars: Vec<(String, String)> = std::env::vars().collect();
for (k, v) in extra {
vars.retain(|(name, _)| !name.eq_ignore_ascii_case(k));
vars.push((k.clone(), v.clone()));
}
vars.sort_by(|a, b| a.0.to_uppercase().cmp(&b.0.to_uppercase()));
let mut block: Vec<u16> = Vec::new();
for (k, v) in vars {
block.extend(format!("{k}={v}").encode_utf16());
block.push(0);
}
block.push(0);
block
}
/// Spawn a GUI program with STARTF_USESHOWWINDOW = SW_SHOWNOACTIVATE so its first window
/// opens behind the user's work instead of grabbing the foreground (best effort: an app can
/// override nCmdShow, which is why the etiquette layer also watches). stdout and stderr go
/// to `log` when given (KiCad writes everything it has to say to stderr; a GUI process has
/// no console, so without this the only diagnostics channel is thrown away). Detached: no
/// console inherited, and the child outlives the bridge.
pub fn spawn_background(exe: &Path, args: &[String], env: &[(String, String)], log: Option<&Path>) -> Result<u32, String> {
if !exe.is_file() {
return Err(format!("{} does not exist", exe.display()));
}
let exe_s = exe.to_string_lossy().to_string();
let mut cmdline = quote_arg(&exe_s);
for a in args {
cmdline.push(' ');
cmdline.push_str("e_arg(a));
}
let app = wide(&exe_s);
let mut cmd = wide(&cmdline);
let mut si = STARTUPINFOW { cb: std::mem::size_of::<STARTUPINFOW>() as u32, ..Default::default() };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOWNOACTIVATE.0 as u16;
// Keep the log file open across CreateProcessW; the child inherits a duplicate.
let log_file = match log {
Some(p) => {
let f = std::fs::OpenOptions::new().create(true).append(true).open(p).map_err(|e| format!("open log {}: {e}", p.display()))?;
let h = HANDLE(f.as_raw_handle() as *mut _);
// SAFETY: `h` is the live handle of `f`, which outlives the spawn.
unsafe { SetHandleInformation(h, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT) }.map_err(|e| format!("SetHandleInformation: {e}"))?;
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = HANDLE::default();
si.hStdOutput = h;
si.hStdError = h;
Some(f)
}
None => None,
};
let inherit = log_file.is_some();
let block = if env.is_empty() { None } else { Some(env_block(env)) };
let mut pi = PROCESS_INFORMATION::default();
// SAFETY: `app`, `cmd`, `block`, `si` and `pi` all outlive the call; `cmd` is a mutable
// NUL-terminated buffer as CreateProcessW requires; the environment block is
// double-NUL-terminated UTF-16 and flagged as such.
let res = unsafe {
CreateProcessW(
PCWSTR(app.as_ptr()),
Some(PWSTR(cmd.as_mut_ptr())),
None,
None,
inherit,
CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS,
block.as_ref().map(|b| b.as_ptr() as *const _),
PCWSTR::null(),
&si,
&mut pi,
)
};
drop(log_file);
res.map_err(|e| format!("CreateProcessW({}): {e}", exe.display()))?;
// SAFETY: both handles were returned by a successful CreateProcessW and are closed once.
unsafe {
let _ = CloseHandle(pi.hThread);
let _ = CloseHandle(pi.hProcess);
}
Ok(pi.dwProcessId)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoting_matches_msvcrt_rules() {
assert_eq!(quote_arg("plain"), "plain");
assert_eq!(quote_arg(""), "\"\"");
assert_eq!(quote_arg("C:\\Users\\john\\My Board\\b.kicad_pcb"), "\"C:\\Users\\john\\My Board\\b.kicad_pcb\"");
assert_eq!(quote_arg("say \"hi\""), "\"say \\\"hi\\\"\"");
assert_eq!(quote_arg("trail\\ "), "\"trail\\ \"");
assert_eq!(quote_arg("end\\"), "end\\");
assert_eq!(quote_arg("a b\\"), "\"a b\\\\\"");
}
#[test]
fn base_names() {
assert_eq!(base_name("C:\\Program Files\\KiCad\\10.0\\bin\\PCBNEW.exe"), "pcbnew.exe");
assert_eq!(base_name("C:/x/kicad.exe"), "kicad.exe");
assert_eq!(base_name(""), "");
}
#[test]
fn env_block_is_double_terminated_and_overrides() {
let b = env_block(&[("KICAD_ENABLE_WXTRACE".into(), "1".into())]);
assert_eq!(&b[b.len() - 2..], &[0, 0]);
let s = String::from_utf16_lossy(&b);
assert!(s.contains("KICAD_ENABLE_WXTRACE=1\0"));
}
}