master
John Lauer rust: Windows window-control layer (enumerate, process, messages, menu, focus, capture, UIA), all cross-process calls h… 3ecdded 2mo ago
//! 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(&quote_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"));
    }
}