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: show family, progress registry (verb_times, operations), kicad_upgrade with silent install; alpha.3, 94 verbs, 18…
46c748c
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
//! Silent KiCad install primitives (phase 3b), ported from handlers/upgrade.py:
//! `_elevation_facts` (token elevation query plus the two UAC registry values) and the
//! `proc.run([installer, scope, "/S"], timeout=900)` call with a kill on the deadline.
//!
//! The NsisMultiUser installer KiCad 10 ships REQUIRES a scope flag next to `/S`: bare
//! `/S` is "invalid command-line parameters" and exits instantly with rc=666660 (its
//! sibling 666661 is "elevation restricted"). `/allusers` needs an elevated token;
//! `/currentuser` installs to %LOCALAPPDATA%\Programs\KiCad with no admin and no UAC.
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde_json::{json, Value};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Security::{GetTokenInformation, TokenElevation, TokenElevationType, TOKEN_ELEVATION, TOKEN_QUERY};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows::Win32::UI::Shell::IsUserAnAdmin;
/// The official downloads.kicad.org x86_64 NSIS installer (handlers/upgrade.py `_DL_URL`).
pub const DL_URL: &str = "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-{v}-x86_64.exe";
pub fn download_url(version: &str) -> String {
DL_URL.replace("{v}", version)
}
/// TokenElevationType (18) and TokenElevation (20) of this process's token, as
/// (elevation_type, is_elevated). Err carries the failing call.
fn token_facts() -> Result<(u32, u32), String> {
let mut token = HANDLE::default();
// SAFETY: GetCurrentProcess is a pseudo handle; `token` outlives the call and is
// closed below.
unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }.map_err(|e| format!("OpenProcessToken failed: {e}"))?;
let query = |class| -> Result<u32, String> {
let mut val: u32 = 0;
let mut len: u32 = 0;
// SAFETY: both TOKEN_ELEVATION_TYPE and TOKEN_ELEVATION are a single DWORD;
// `val` and `len` outlive the call.
unsafe { GetTokenInformation(token, class, Some(&mut val as *mut u32 as *mut _), std::mem::size_of::<u32>() as u32, &mut len) }
.map(|()| val)
.map_err(|e| format!("GetTokenInformation failed: {e}"))
};
let kind = query(TokenElevationType);
let elevated = query(TokenElevation);
// SAFETY: the handle came from OpenProcessToken above and is not used afterwards.
let _ = unsafe { CloseHandle(token) };
debug_assert_eq!(std::mem::size_of::<TOKEN_ELEVATION>(), std::mem::size_of::<u32>());
Ok((kind?, elevated?))
}
fn uac_registry() -> (Value, Value) {
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
let root = RegKey::predef(HKEY_LOCAL_MACHINE);
match root.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System") {
Ok(k) => {
let lua: Value = match k.get_value::<u32, _>("EnableLUA") {
Ok(v) => json!(v),
Err(e) => json!(format!("error: {e}")),
};
// Absent means the Windows default (5: prompt for consent on the secure desktop).
let consent: Value = match k.get_value::<u32, _>("ConsentPromptBehaviorAdmin") {
Ok(v) => json!(v),
Err(_) => json!("not set (default 5: prompt)"),
};
(lua, consent)
}
Err(e) => (json!(format!("error: {e}")), Value::Null),
}
}
/// handlers/upgrade.py `_elevation_facts`, plus the four summary keys the trait promises.
pub fn elevation_info() -> Result<Value, String> {
let mut facts = json!({
"platform": "win32",
"user": std::env::var("USERNAME").unwrap_or_default(),
});
// SAFETY: shell32 query with no arguments.
let shell_admin = unsafe { IsUserAnAdmin() }.as_bool();
facts["isUserAnAdmin"] = json!(shell_admin);
let mut token_elevated: Option<bool> = None;
match token_facts() {
Ok((kind, elevated)) => {
facts["tokenElevationType"] = json!(kind);
facts["tokenElevated"] = json!(elevated);
facts["tokenElevationTypeMeaning"] = json!(match kind {
1 => "Default: no split token (UAC disabled machine-wide, or built-in Administrator): full admin, NO UAC prompt ever",
2 => "Full: elevated via an approved UAC prompt (or elevated parent)",
3 => "Limited: standard filtered token, NOT admin",
_ => "unknown",
});
token_elevated = Some(elevated != 0);
}
Err(e) => facts["tokenError"] = json!(e),
}
let (lua, consent) = uac_registry();
facts["uacEnableLUA"] = lua.clone();
facts["uacConsentPromptBehaviorAdmin"] = consent.clone();
// Summary. isAdmin is the token's own word when we could read it, else shell32's.
let is_admin = token_elevated.unwrap_or(shell_admin);
let uac_enabled: Value = match lua.as_u64() {
Some(v) => json!(v != 0),
None => Value::Null,
};
facts["isAdmin"] = json!(is_admin);
facts["uacEnabled"] = uac_enabled;
facts["consentPrompt"] = match consent.as_u64() {
Some(v) => json!(v),
None => Value::Null,
};
// /allusers /S runs to completion without a prompt only under an elevated token;
// a limited token gets NsisMultiUser's 666661 (elevation restricted) in silent mode.
facts["canInstallAllUsersSilently"] = json!(is_admin);
Ok(facts)
}
/// `<installer> /allusers|/currentuser /S`, hidden, no console, blocked until it exits or
/// `timeout_secs` passes (then killed). Returns the exit code.
pub fn run_installer(path: &Path, all_users: bool, timeout_secs: u64) -> Result<i32, String> {
if !path.is_file() {
return Err(format!("{} does not exist", path.display()));
}
let scope = if all_users { "/allusers" } else { "/currentuser" };
let mut cmd = Command::new(path);
cmd.arg(scope).arg("/S");
cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let mut child = cmd.spawn().map_err(|e| format!("installer launch failed: {e}"))?;
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
loop {
match child.try_wait() {
Ok(Some(status)) => return Ok(status.code().unwrap_or(-1)),
Ok(None) => {}
Err(e) => return Err(format!("waiting on the installer failed: {e}")),
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Err(format!("installer did not finish within {timeout_secs} s; killed. A silent NSIS run that stalls this long is usually waiting on a UAC prompt nobody can see, or SmartScreen/AV holding the exe"));
}
std::thread::sleep(Duration::from_millis(250));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_matches_python() {
assert_eq!(download_url("10.0.6"), "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-10.0.6-x86_64.exe");
}
#[test]
fn elevation_facts_are_concrete() {
let f = elevation_info().unwrap();
assert!(f["isAdmin"].is_boolean());
assert!(f["canInstallAllUsersSilently"].is_boolean());
assert!(f["isUserAnAdmin"].is_boolean());
assert!(f.get("uacEnableLUA").is_some());
assert!(f.get("uacConsentPromptBehaviorAdmin").is_some());
// The token is ours to read; a failure here is a real regression.
assert!(f["tokenElevationType"].is_u64(), "{f}");
assert!(f["tokenElevated"].is_u64(), "{f}");
}
#[test]
fn missing_installer_is_refused() {
assert!(run_installer(Path::new(r"C:\nope\kicad-0.0.0-x86_64.exe"), false, 5).is_err());
}
}