app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641
//! THE GRACEFUL QUIT LADDER — `swx_stop` v2.
//!
//! Full design and evidence: `docs/QUIT-INVESTIGATION.md` §5. This module holds
//! the rules that must be decidable without a machine (so they are testable) and
//! the measurements that come from the machine rather than from a COM return
//! value (so they cannot lie).
//!
//! ## The three laws
//!
//! 1. **NEVER A KILL.** No rung reaches `taskkill`, `TerminateProcess`,
//! `swx_kill`, or `RmForceShutdown`. `swx_kill` is a separate, differently
//! gated verb; `swx_stop` must never call it, escalate to it, or recommend it
//! in a hint. `RmForceShutdown` is not a runtime flag here — the Restart
//! Manager rung passes a literal `0`.
//! 2. **NEVER ANSWER A DIALOG.** The census runs before every rung and again
//! after each one. Any top-level dialog owned by the SOLIDWORKS pid stops the
//! ladder and is REPORTED, never clicked, never Escaped, never `WM_CLOSE`d.
//! On 2026-09-02 closing a stuck SOLIDWORKS modal through the window manager
//! took the whole application down.
//! 3. **NOTHING IS DISOWNED UNTIL THE PROCESS IS VERIFIABLY GONE.** v1 cleared
//! the created-document registry *before* it verified the quit, so a declined
//! quit left the bridge unable to touch its own documents. Success is
//! (pid + creation time) absent from a live census — never a COM call, never
//! `GetActiveObject`.
//!
//! ## Why the reference release is load-bearing
//!
//! An automation client's outstanding `IDispatch` is exactly what keeps a
//! SOLIDWORKS alive (`UserControl` semantics, §1.2). v1 asked "are you still
//! there?" while holding a live reference for the whole match arm, so it could
//! not tell "declined" from "quit, but pinned by our own pointer". Worse, a
//! `WM_CLOSE` sent while we still hold one is the most direct way to manufacture
//! the half-closed `SLDWORKS.exe -Embedding` orphan this design exists to avoid.
//!
//! The refcount reasoning, stated once: `variant::Disp` wraps a `windows`-crate
//! `IDispatch`, which is a refcounted COM pointer — `Clone` calls `AddRef` and
//! `Drop` calls `Release`. The com host holds at most ONE cached reference
//! (`ComBackend::sw`) plus whatever locals a command creates. So "release
//! everything" is: drop every local `Disp` (end of scope) and set `self.sw =
//! None`. `release_com` does exactly that and then reports
//! `outstanding: 0`, which the ladder requires before any rung and before any
//! measurement. Re-attaching for a later measurement would defeat the point, so
//! the measurement never touches COM at all.
use crate::launchos::{ProcInfo, WindowInfo};
use serde_json::{json, Value};
/// `swCommands_App_Exit` — File ▸ Exit, through the app's own command
/// dispatcher. Source: SOLIDWORKS API `swCommands_e` (see
/// docs/QUIT-INVESTIGATION.md §1.5). Not inline anywhere.
pub const SW_COMMANDS_APP_EXIT: i32 = 2825;
/// The rungs, in order. There is no rung (g): the ladder ends in an honest
/// refusal, never in a kill.
pub const RUNGS: &[&str] = &["a", "b", "c", "d", "e", "f"];
/// How far the ladder may climb unless the caller says otherwise.
///
/// **"a" until the live gate has run.** Shipping the whole ladder armed on day
/// one would be exactly the mistake crash-reporter recovery avoided by shipping
/// `off`: rungs (b)-(f) send window messages and run VBA inside somebody's CAD
/// session, and not one of them has been observed on a real machine.
pub const DEFAULT_MAX_RUNG: &str = "a";
/// What each rung actually does, for the report and the ledger.
pub fn rung_action(rung: &str) -> &'static str {
match rung {
"a" => "UserControl=false; ExitApp; release every COM reference",
"b" => "RunCommand(swCommands_App_Exit=2825)",
"c" => "SendMessageTimeout(WM_CLOSE) to the main frame",
"d" => "PostMessage(WM_SYSCOMMAND, SC_CLOSE) to the main frame",
"e" => "RunMacro2 of the shipped two-line exit macro",
"f" => "Restart Manager RmShutdown (RmForceShutdown OMITTED)",
_ => "unknown rung",
}
}
/// Per-rung budget in seconds, from §5.3.
pub fn rung_budget_secs(rung: &str) -> u64 {
match rung {
"a" | "b" => 30,
"c" | "d" => 45,
"e" => 60,
"f" => 90,
_ => 30,
}
}
pub fn is_rung(r: &str) -> bool {
RUNGS.contains(&r)
}
/// The rungs to try, in order, up to and including `max`. An unknown `max`
/// yields nothing — the caller refuses rather than guessing.
pub fn rungs_up_to(max: &str) -> Vec<&'static str> {
match RUNGS.iter().position(|r| *r == max) {
None => Vec::new(),
Some(i) => RUNGS[..=i].to_vec(),
}
}
/// The per-session children a GRACEFUL exit reaps. Their survival after the pid
/// is gone means the exit was abrupt, not clean.
pub const SESSION_CHILDREN: &[&str] =
&["sldProcMon.exe", "sldsurfacing.exe", "sldimport.exe", "swCefSubProc.exe"];
/// The processes that MUST SURVIVE a graceful quit — by design (§2.6).
///
/// Their continued presence is CONSENT, not a failure: they are the platform's
/// own daemons and the user's licence/vault plumbing, none of which belongs to
/// the SOLIDWORKS session the bridge is ending. Reporting them as "not cleaned
/// up" would invite somebody to kill them, which is the launch path's oldest
/// lesson (`docs/LAUNCH-RESILIENCE.md` §3).
pub const EXPECTED_SURVIVORS: &[&str] = &[
"ENOPLMCSAClient.exe",
"EdmServerV6.exe",
"SWXDesktopHelper.exe",
"sldBgDwld.exe",
"3DEXPERIENCELauncher.exe",
"3DEXPERIENCELauncherSysTray.exe",
"3DEXPERIENCELauncherService.exe",
];
/// Images whose behaviour at exit is undocumented: RECORD, DO NOT JUDGE (§5.4).
pub const UNJUDGED: &[&str] = &["ViewServerV6.exe", "CATSTART.exe", "CATSysDemon.exe"];
/// The identity of the session being quit, captured BEFORE the first rung (P7).
///
/// Same attribution law as the launch path: without a pid **and** its creation
/// time, "the process went away" cannot be told from "a pid was reused", and
/// every rung's success measurement depends on that distinction.
#[derive(Clone, Debug, PartialEq)]
pub struct Identity {
pub pid: u32,
pub created_at: u64,
pub main_hwnd: Option<i64>,
}
impl Identity {
pub fn to_json(&self) -> Value {
json!({"processId": self.pid, "processCreatedAt": self.created_at, "mainHwnd": self.main_hwnd})
}
}
/// IS THE PROCESS GONE? The one success criterion.
///
/// `None` means the census could not be read — which is a rung that FAILED to be
/// measured, never a rung that worked. A pid alone is not enough: a matching pid
/// with a DIFFERENT creation time is a new process, and the session we were
/// quitting is gone.
pub fn process_gone(census: Result<&Vec<ProcInfo>, &String>, id: &Identity) -> Option<bool> {
let c = census.ok()?;
Some(!c.iter().any(|p| p.pid == id.pid && p.created_at == Some(id.created_at)))
}
/// EVERY PID A BLOCKING DIALOG COULD BELONG TO: SOLIDWORKS itself, plus the CEF
/// (browser) subprocesses it started.
///
/// The 3DEXPERIENCE sign-in, licence and cloud-save windows are web views, and a
/// web view's window belongs to `swCefSubProc.exe` — a CHILD of `sldworks.exe`,
/// with its own pid. A census of the SOLIDWORKS pid alone cannot see them, so
/// "no dialog is on screen" was provable while the platform was holding a modal
/// asking the user to sign in. Attribution is by parentage, never by class name:
/// only a `swCefSubProc.exe` whose ppid IS this session's pid counts, so another
/// SOLIDWORKS session's web view is never mistaken for ours.
///
/// `None` when the census could not be read — which makes the dialog question
/// UNKNOWN, and UNKNOWN refuses.
pub fn dialog_census_pids(census: Result<&Vec<ProcInfo>, &String>, id: &Identity) -> Option<Vec<u32>> {
let c = census.ok()?;
let mut pids = vec![id.pid];
for p in c.iter() {
if p.image.eq_ignore_ascii_case("swCefSubProc.exe") && p.ppid == Some(id.pid) {
pids.push(p.pid);
}
}
Some(pids)
}
/// The per-session children still alive, and the expected survivors still alive.
pub fn reap_report(census: &[ProcInfo]) -> (Vec<String>, Vec<String>, Vec<String>) {
let names = |set: &[&str]| -> Vec<String> {
census
.iter()
.filter(|p| set.iter().any(|s| s.eq_ignore_ascii_case(&p.image)))
.map(|p| p.image.clone())
.collect()
};
(names(SESSION_CHILDREN), names(EXPECTED_SURVIVORS), names(UNJUDGED))
}
/// IS THIS HWND STILL THE WINDOW WE MEASURED? Asked immediately before a message
/// rung sends anything.
///
/// Window handles are recycled. Rungs (c) and (d) send to an hwnd captured at
/// precondition time, and between then and the send SOLIDWORKS may have gone
/// (rung (a) is the rung before them, and its whole purpose is to make it go) —
/// at which point that handle can belong to somebody else's window, and
/// `WM_CLOSE` to somebody else's window is a close nobody asked for. Both facts
/// must answer: the window's owning pid, and that pid's creation time.
pub fn hwnd_still_owned(
hwnd_pid: Option<u32>,
hwnd_pid_created_at: Option<u64>,
id: &Identity,
) -> bool {
hwnd_pid == Some(id.pid) && hwnd_pid_created_at == Some(id.created_at)
}
/// A dialog that stops the ladder, classified — and NEVER answered.
#[derive(Clone, Debug, PartialEq)]
pub struct BlockingDialog {
pub hwnd: i64,
pub title: String,
pub kind: &'static str,
/// WHAT IT SAYS AND WHAT IT OFFERS. `None` = not read, or unreadable —
/// UNKNOWN, never "it said nothing".
pub contents: Option<crate::launchos::DialogContents>,
/// The window as measured — class, owner, whether the owner is enabled,
/// styles, rect — so a window nobody recognises can be judged from the reply.
pub window: Option<WindowInfo>,
/// Does it block? Decided by `judge_window` from the evidence, with the reason.
pub blocking: bool,
pub why: String,
/// THE OWNER-STATE SETTLE (0.9.8): when ownerEnabled was the ONLY fact that
/// made this window blocking, the re-reads of its owner's enabled state —
/// `{reads:[{atMs, enabled}], ownerDisabledTransiently}`. `None` = no settle
/// was needed or allowed.
pub owner_settle: Option<Value>,
}
/// The captions a custom-drawn window's dialog TEMPLATE carries — measured
/// 2026-10-01 on the 3DEXPERIENCE "Connected to …" notice, whose Static controls
/// read "Title", "Description", "Button1", "Button2", "Hyperlink Text 1",
/// "Hyperlink Text 2" while the window SHOWED something else entirely. When every
/// readable caption is one of these, the text is not the window's words.
pub const TEMPLATE_PLACEHOLDERS: &[&str] =
&["Title", "Description", "Button1", "Button2", "Hyperlink Text 1", "Hyperlink Text 2"];
pub fn text_is_template_placeholder(c: &crate::launchos::DialogContents) -> bool {
let all: Vec<&String> = c.text.iter().chain(c.buttons.iter()).collect();
!all.is_empty() && all.iter().all(|t| TEMPLATE_PLACEHOLDERS.contains(&t.trim()))
}
/// DOES THIS WINDOW BLOCK? Judged by MEASURED MODALITY, not by "a window exists"
/// (0.9.4). Measured 2026-10-01: a title-less `#32770` (hwnd 331370) stopped
/// every quit for minutes — and it was the 3DEXPERIENCE "Connected to …"
/// NOTIFICATION: owner frame ENABLED for all 20 reads over 95 s, WS_EX_TOOLWINDOW,
/// no Button-class child, custom-drawn. A window BLOCKS when:
/// * its TITLE or its TEXT says it is a save prompt, a recovery or an import
/// diagnostics prompt (never overridden by any evidence); or
/// * its title says another known blocking kind (update, settings, message,
/// export options) — a licence-type title alone is the exception (0.9.5,
/// adversary pass 43): measured notification evidence overrides it, because
/// the Connected notice with a title would block every quit and the real
/// License Manager blocks on its Button-class OK anyway; or
/// * its owner frame is DISABLED — a real modal; or
/// * it has a Button-class child (hidden ones count) — something on offer; or
/// * it shows TWO OR MORE button-like captions (Save, Don't Save, Cancel, Yes,
/// No, OK, Retry, …) in any control class, visible or hidden — SOLIDWORKS
/// draws many controls itself. The measured template placeholders
/// ("Button1", "Hyperlink Text 1", "Title", …) are not button-like.
/// It is a `notification` — reported, never answered, and NOT a reason to refuse
/// a quit, a sweep or an import — only when ALL of these were MEASURED: it is a
/// tool window, its owner frame is enabled, its controls were read, none is a
/// Button and fewer than two captions offer a choice. Anything unread FAILS
/// CLOSED: blocking.
///
/// OPEN GAPS that pre-date 0.9.4 (named by adversary pass 43): a modal whose
/// class is not `#32770` is never judged at all, and a prompt drawn INSIDE the
/// main frame (PropertyManager, task pane) is a child window the census never
/// sees. What bounds the damage there: a sweep saves every document before it
/// closes anything, and swx_stop requires zero unsaved documents.
pub fn judge_window(
w: &WindowInfo,
contents: Option<&crate::launchos::DialogContents>,
) -> (bool, &'static str, String) {
let title_kind = crate::abwin::classify(&w.title).0;
// ONE CLASSIFIER for the title AND the words (adversary passes 43 and 44):
// the same function swx_dismiss_blocking_dialogs uses before it closes.
let (words_kind, source, _, _) = classify_words(&w.title, contents);
// A save, recovery or import prompt blocks whatever the evidence says — by
// title OR by text. No measured modality overrides these.
if NEVER_OVERRIDDEN_KINDS.contains(&words_kind) {
return (true, words_kind, format!("a known blocking kind ({words_kind}) by its {source}"));
}
// Any other title kind blocks by title — EXCEPT a licence-type title, which
// measured notification evidence may override (W2): the 3DEXPERIENCE
// "Connected" notice with a title would otherwise block every quit again,
// and the real License Manager blocks on its Button-class OK regardless.
if title_kind != "unknown" && title_kind != "license" {
return (true, title_kind, format!("a known blocking kind ({title_kind}) by its title"));
}
let kind = title_kind;
match w.owner_enabled {
Some(false) => return (true, kind, "its owner frame is DISABLED: a modal".into()),
None => return (true, kind, "whether its owner frame is enabled could not be read".into()),
Some(true) => {}
}
let Some(c) = contents else {
return (true, kind, "its controls could not be read".into());
};
if c.has_button_class_child() {
return (true, kind, "it has a Button-class control: something on offer".into());
}
let offered = button_like_captions(c);
if offered.len() >= 2 {
return (
true,
kind,
format!(
"it shows {} button-like captions ({}) — something is on offer, whatever class \
draws them",
offered.len(),
offered.join(", ")
),
);
}
match w.exstyle {
Some(x) if x & crate::launchos::WS_EX_TOOLWINDOW != 0 => (
false,
"notification",
if kind == "license" {
"a licence-type TITLE, but measured notification evidence overrides a title-only \
guess: a tool window whose owner frame is ENABLED, controls read, no Button-class \
control and no button-like captions"
.into()
} else {
"a tool window whose owner frame is ENABLED and which has no Button-class control \
and no button-like captions: a notification, not a modal"
.into()
},
),
Some(_) => (true, kind, "not a tool window: not provably a notification".into()),
None => (true, kind, "its extended style could not be read".into()),
}
}
/// THE ONE CLASSIFIER over a window's TITLE and its WORDS (text and button
/// captions): `(kind, source, blocking, neverAutoAnswer)`. A save, recovery or
/// import kind from EITHER wins; otherwise the title's kind; otherwise the first
/// kind the words name; otherwise `unknown` (never auto-answered). `source` is
/// "title", "text" or "none". Used by `judge_window` and by
/// swx_dismiss_blocking_dialogs, so a save question in a window titled
/// "Warning" is a save question to both.
pub fn classify_words(
title: &str,
contents: Option<&crate::launchos::DialogContents>,
) -> (&'static str, &'static str, bool, bool) {
let t = crate::abwin::classify(title);
let words: Vec<(&'static str, bool, bool)> = contents
.map(|c| {
c.text
.iter()
.chain(c.buttons.iter())
.map(|x| crate::abwin::classify(x))
.filter(|k| k.0 != "unknown")
.collect()
})
.unwrap_or_default();
if NEVER_OVERRIDDEN_KINDS.contains(&t.0) {
return (t.0, "title", t.1, t.2);
}
if let Some(k) = words.iter().find(|k| NEVER_OVERRIDDEN_KINDS.contains(&k.0)) {
return (k.0, "text", k.1, k.2);
}
if t.0 != "unknown" {
return (t.0, "title", t.1, t.2);
}
if let Some(k) = words.first() {
return (k.0, "text", k.1, k.2);
}
(t.0, "none", t.1, t.2)
}
/// Kinds that block whatever the measured evidence says — a choice about the
/// user's work (save), their recovered work (recovery), or the geometry an
/// import produces.
pub const NEVER_OVERRIDDEN_KINDS: &[&str] = &["save_changes", "document_recovery", "import_diagnostics"];
/// Captions that offer a choice. Matched whole, case-insensitive, with `&`
/// accelerators and trailing dots removed. The measured TEMPLATE PLACEHOLDERS
/// ("Button1", "Button2", "Hyperlink Text 1/2", "Title", "Description") are NOT
/// button-like: they are what the Connected notice's template left behind.
pub const BUTTON_LIKE: &[&str] = &[
"save", "don't save", "do not save", "dont save", "save all", "cancel", "yes", "no", "ok",
"retry", "abort", "ignore", "continue", "close", "discard", "yes to all", "no to all",
];
/// The button-like captions among a window's text and buttons, any control
/// class, visible or hidden (the text read includes hidden controls).
pub fn button_like_captions(c: &crate::launchos::DialogContents) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for t in c.text.iter().chain(c.buttons.iter()) {
let n = t
.replace('&', "")
.replace('\u{2019}', "'")
.trim()
.trim_end_matches(|ch| ch == '.' || ch == '\u{2026}')
.trim()
.to_lowercase();
if BUTTON_LIKE.contains(&n.as_str()) && !out.iter().any(|o| o.to_lowercase() == n) {
out.push(t.trim().to_string());
}
}
out
}
/// THE DIALOG CENSUS, JUDGED: every visible `#32770`, its contents read, each
/// judged. `blocking` is the first that blocks; `notifications` are the ones
/// that do not — reported, never answered.
pub struct DialogCensus {
pub blocking: Option<BlockingDialog>,
pub notifications: Vec<BlockingDialog>,
/// EVERY OTHER blocking dialog (0.11.1): `blocking` is the first, which
/// decides a refusal; a census that reports never drops the rest.
pub more_blocking: Vec<BlockingDialog>,
}
impl DialogCensus {
/// Every blocking dialog, the first first.
pub fn all_blocking(&self) -> Vec<&BlockingDialog> {
self.blocking.iter().chain(self.more_blocking.iter()).collect()
}
}
/// How many times, and how far apart, a disabled owner is re-read before a
/// window that is a notification on every other fact is called a modal.
pub const OWNER_SETTLE_READS: u32 = 5;
fn owner_settle_ms() -> u64 {
std::env::var("SWX_OWNER_SETTLE_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(400)
}
/// MODALITY IS A PERSISTENT MEASUREMENT, NOT ONE READ (0.9.8). MEASURED
/// 2026-10-01 on 0.9.7: the 3DEXPERIENCE Connected notice (same class, styles,
/// size and placeholder captions as the morning's) was read with its owner frame
/// DISABLED — at an instant when SOLIDWORKS was busy inside the sweep's own
/// close/save — and stopped the sweep as a modal; seconds later the frame was
/// enabled again. So when a window is a notification on EVERY other measured
/// fact (tool window, controls read, no Button class, no button-like captions,
/// no save/recovery/import words) and ONLY its owner's enabled state says
/// blocking, the owner is re-read up to OWNER_SETTLE_READS times ~400 ms apart:
/// disabled (or unreadable) on every read -> blocking, as before; enabled on a
/// later read -> a notification, reported with `ownerDisabledTransiently:true`
/// and the read history. The settle is never applied to a window with a
/// Button-class child, button-like captions, or a save/recovery/import kind —
/// `judge_window` with the owner taken as enabled still blocks those. The census
/// runs between this bridge's own COM calls: no call of the sweep is in flight
/// while it reads.
fn settle_owner(w: &WindowInfo, contents: Option<&crate::launchos::DialogContents>, os: &dyn crate::launchos::LaunchOs) -> Option<(bool, Value)> {
if w.owner_enabled != Some(false) {
return None;
}
let mut assumed = w.clone();
assumed.owner_enabled = Some(true);
if judge_window(&assumed, contents).0 {
return None; // something other than the owner's state blocks it
}
let mut reads = vec![json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": false, "read": "census"})];
for _ in 0..OWNER_SETTLE_READS {
std::thread::sleep(std::time::Duration::from_millis(owner_settle_ms()));
let e = os.owner_enabled_of(w.hwnd);
reads.push(json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": e}));
if e == Some(true) {
return Some((true, json!({"reads": reads, "ownerDisabledTransiently": true})));
}
}
Some((false, json!({"reads": reads, "ownerDisabledTransiently": false})))
}
pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs) -> DialogCensus {
let mut out = DialogCensus { blocking: None, notifications: Vec::new(), more_blocking: Vec::new() };
for w in windows.iter().filter(|w| w.is_dialog() && w.visible) {
let contents = os.dialog_contents(w.hwnd).ok();
// A body that could not be read is judged as unread (0.9.12).
let readable = contents.as_ref().filter(|c| !c.body_unreadable());
let (mut blocking, mut kind, mut why) = judge_window(w, readable);
let mut owner_settle = None;
if blocking {
if let Some((enabled_later, history)) = settle_owner(w, readable, os) {
if enabled_later {
let mut assumed = w.clone();
assumed.owner_enabled = Some(true);
let (b, k, y) = judge_window(&assumed, readable);
blocking = b;
kind = k;
why = format!(
"{y} — its owner frame read DISABLED at first and ENABLED on a re-read: \
SOLIDWORKS was busy, not held by this window (ownerDisabledTransiently)"
);
} else {
why = format!(
"{why} — on every one of {} re-reads over the settle window",
OWNER_SETTLE_READS
);
}
owner_settle = Some(history);
}
}
let d = BlockingDialog {
hwnd: w.hwnd,
title: w.title.clone(),
kind,
contents,
window: Some(w.clone()),
blocking,
why,
owner_settle,
};
if blocking {
if out.blocking.is_none() {
out.blocking = Some(d);
} else {
out.more_blocking.push(d);
}
} else {
out.notifications.push(d);
}
}
out
}
impl BlockingDialog {
pub fn to_json(&self) -> Value {
let c = self.contents.as_ref();
// A TEMPLATE'S CAPTIONS ARE NOT THE WINDOW'S WORDS (measured 2026-10-01).
let placeholder = c.map(text_is_template_placeholder).unwrap_or(false);
let body_unreadable = c.map(|c| c.body_unreadable()).unwrap_or(false);
json!({
"hwnd": self.hwnd,
"title": self.title,
"kind": self.kind,
"blocking": self.blocking,
"why": self.why,
"textIsTemplatePlaceholder": placeholder,
"placeholderCaptions": if placeholder { c.map(|c| json!(c.text)).unwrap_or(Value::Null) } else { Value::Null },
"evidence": {
"window": self.window.as_ref().map(|w| w.to_json()),
"controls": c.map(|c| c.to_json()["controls"].clone()),
},
// A TITLE IS NOT THE MESSAGE. Live 2026-09-17 the sweep stopped on
// `{"kind":"license","title":"License Manager"}` — correct, and
// unactionable: the window actually said the licence SERVER was
// unreachable and the session would end after five more failed
// attempts. That is a countdown somebody needs to see.
"text": if placeholder || body_unreadable { Value::Null } else { c.map(|c| json!(c.text)).unwrap_or(Value::Null) },
"buttons": if body_unreadable { Value::Null } else { c.map(|c| json!(c.buttons)).unwrap_or(Value::Null) },
// HONEST (0.9.12): a DirectUIHWND task dialog's words are not in
// child controls, so a walk that read none of them did not read it.
"contentsReadable": c.is_some() && !body_unreadable,
"contentsUnreadableWhy": if body_unreadable {
json!("its body is a DirectUIHWND task dialog (or has no child controls): its words are not \
in child controls this bridge can read — take a screenshot to see them")
} else if c.is_none() { json!("the controls could not be read") } else { Value::Null },
// Whether the only thing on offer is acknowledgement. This bridge
// still NEVER presses it — see `never_answer` — and whether it ever
// may is a question for the user, not an inference from a control
// tree.
"acknowledgeOnly": c.map(|c| json!(c.acknowledge_only())).unwrap_or(Value::Null),
"ownerDisabledTransiently": self.owner_settle.as_ref().and_then(|o| o.get("ownerDisabledTransiently").cloned()).unwrap_or(json!(false)),
"ownerReads": self.owner_settle.as_ref().and_then(|o| o.get("reads").cloned()).unwrap_or(Value::Null),
"neverAnswer": true,
// WHICH WINDOW, AND WHERE IT CAME FROM (0.11.1): an operator facing a
// refusal sees exactly which dialog, whose it is, how long it has been
// up, and whether a bridge verb raised it.
"className": self.window.as_ref().map(|w| w.class_name.clone()),
"owner": self.window.as_ref().and_then(|w| w.owner),
"rect": self.window.as_ref().and_then(|w| w.rect),
"visible": self.window.as_ref().map(|w| w.visible),
"attribution": crate::dialogs::attribution(self.hwnd, crate::launchos::os().as_ref()),
})
}
/// `to_json`, plus what the bridge may do about it: the standard file
/// dialog it provably raised, with an empty name, is the one it may cancel.
pub fn report(&self, os: &dyn crate::launchos::LaunchOs) -> Value {
let mut v = self.to_json();
let fd = os.file_dialog(self.hwnd).ok();
v["fileDialog"] = match &fd {
Some(f) => json!({"standard": f.standard, "fileNameEmpty": f.file_name.as_ref().map(|n| n.trim().is_empty())}),
None => Value::Null,
};
match crate::dialogs::may_cancel(self.hwnd, os) {
Ok((verb, _)) => {
v["bridgeMayCancel"] = json!(true);
v["cancelBecause"] = json!(format!(
"the standard file dialog with an EMPTY file name, raised during {verb} with no user input since: swx_dismiss_blocking_dialogs {{close:true, confirm:true, reason}} cancels it (Cancel changes nothing)"
));
}
Err(why) => {
v["bridgeMayCancel"] = json!(false);
v["notCancellableBecause"] = json!(why);
v["whoAnswers"] = json!("the user cancels it");
}
}
v
}
/// READ WHAT IT SAYS. Read-only; a failure leaves `contents` as `None`.
pub fn read_contents(&mut self, os: &dyn crate::launchos::LaunchOs) {
self.contents = os.dialog_contents(self.hwnd).ok();
}
/// The error code this dialog produces. A save prompt is called out
/// separately because at a VERIFIED zero documents it should be impossible —
/// its appearance means our document count was wrong, which is a bug to
/// report, not a dialog to dismiss.
pub fn error_code(&self) -> &'static str {
match self.kind {
"save_changes" => "stop_blocked_by_save_prompt",
_ => "stop_blocked_by_dialog",
}
}
}
/// The first blocking dialog in a window census, if any.
///
/// Only top-level `#32770` windows owned by the SOLIDWORKS pid count — the
/// census is already scoped to that pid, and the main frame is an `Afx:` class,
/// never a dialog.
pub fn first_blocking_dialog(windows: &[WindowInfo]) -> Option<BlockingDialog> {
windows.iter().filter(|w| w.is_dialog() && w.visible).map(|w| {
let (kind, _blocking, _never) = crate::abwin::classify(&w.title);
BlockingDialog {
hwnd: w.hwnd,
title: w.title.clone(),
kind,
contents: None,
window: Some(w.clone()),
blocking: true,
why: "a visible dialog, not yet judged".into(),
owner_settle: None,
}
}).next()
}
/// What a rung reported doing. `Exited` is never trusted on its own — the census
/// is what decides — but a rung that says it was blocked stops the ladder,
/// because an access-denied is a fact about the machine's integrity levels, not
/// about SOLIDWORKS' willingness to quit.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RungResult {
/// The rung ran and the process is gone (census-confirmed).
Quit,
/// The rung ran and the process is still there.
Declined,
/// A top-level dialog appeared. LADDER STOPS.
DialogAppeared,
/// UIPI refused the message. LADDER STOPS — escalating past an
/// access-denied would just try the same blocked gesture another way.
UipiBlocked,
/// The rung could not be attempted (not shipped, not permitted, no hwnd).
Skipped,
/// The rung ran and its effect could not be MEASURED. A rung that cannot be
/// measured is a rung that failed.
Unmeasured,
}
impl RungResult {
pub fn name(&self) -> &'static str {
match self {
RungResult::Quit => "quit",
RungResult::Declined => "declined",
RungResult::DialogAppeared => "dialogAppeared",
RungResult::UipiBlocked => "uipiBlocked",
RungResult::Skipped => "skipped",
RungResult::Unmeasured => "unmeasured",
}
}
/// Does the ladder stop here, whatever the reason?
pub fn stops_ladder(&self) -> bool {
matches!(
self,
RungResult::Quit | RungResult::DialogAppeared | RungResult::UipiBlocked
)
}
}
/// Map the host's own word for what happened onto a result. An unrecognised
/// answer is `Unmeasured`, not `Declined`: "I do not understand the reply" is not
/// "it refused".
pub fn result_from_host(word: &str) -> RungResult {
match word {
"exited" | "quit" => RungResult::Quit,
"declined" => RungResult::Declined,
"dialogAppeared" | "dialog" => RungResult::DialogAppeared,
"uipiBlocked" => RungResult::UipiBlocked,
"skipped" => RungResult::Skipped,
_ => RungResult::Unmeasured,
}
}
/// One rung's full record.
#[derive(Clone, Debug)]
pub struct RungTry {
pub rung: &'static str,
pub action: &'static str,
pub result: RungResult,
pub process_gone: Option<bool>,
pub elapsed_sec: f64,
pub new_windows: usize,
pub dialog: Option<BlockingDialog>,
pub children_alive: Vec<String>,
pub survivors_alive: Vec<String>,
pub unjudged_alive: Vec<String>,
/// Whether the census those three lists came from could be READ. Three empty
/// lists otherwise say "everything was reaped, nothing survived" — the most
/// reassuring possible reading of no information at all.
pub census_readable: bool,
pub detail: Option<String>,
pub host_error: Option<String>,
/// This attempt followed PRIMING a never-had-a-document session (one
/// bridge-owned scratch part created and closed) — measured 2026-10-01.
pub primed: bool,
}
impl RungTry {
pub fn to_json(&self) -> Value {
json!({
"rung": self.rung,
"action": self.action,
"result": self.result.name(),
// `null` when the census could not be read — a rung that cannot be
// measured is a rung that failed, and it says so rather than
// reporting a comfortable false.
"processGone": self.process_gone,
"elapsedSec": (self.elapsed_sec * 10.0).round() / 10.0,
"newWindows": self.new_windows,
"dialog": self.dialog.as_ref().map(|d| d.to_json()),
"sessionChildrenStillAlive": self.children_alive,
"expectedSurvivorsAlive": self.survivors_alive,
"undocumentedStillAlive": self.unjudged_alive,
// The three lists above are only evidence if this is true.
"censusReadable": self.census_readable,
"detail": self.detail,
"hostError": self.host_error,
"primed": self.primed,
})
}
}
/// Why a quit was refused before any rung ran. Every variant carries a code a
/// caller can branch on, and the UNKNOWN ones are deliberately one code: the
/// caller's move is the same for all of them, and it is never "proceed".
#[derive(Clone, Debug, PartialEq)]
pub enum Refusal {
/// A precondition could not be MEASURED. Never proceed.
Unknown { what: &'static str, detail: String },
/// A precondition was measured and is false.
Known { code: &'static str, message: String },
}
impl Refusal {
pub fn code(&self) -> &'static str {
match self {
Refusal::Unknown { .. } => "stop_preconditions_unknown",
Refusal::Known { code, .. } => code,
}
}
pub fn message(&self) -> String {
match self {
Refusal::Unknown { what, detail } => format!(
"cannot verify {what} before quitting SOLIDWORKS, so this bridge will not try: \
{detail}"
),
Refusal::Known { message, .. } => message.clone(),
}
}
}
/// THE PRECONDITION SWEEP, as a pure decision over already-measured facts.
///
/// Every one is required and every one is re-measured immediately before each
/// rung — not inherited from the caller, not cached from an earlier verb. An
/// UNKNOWN in any of them refuses; `force` waives **P2 only**.
///
/// `force` deliberately does NOT waive P1, P3, P4, P5 or P7: those are not
/// questions about the user's data, they are the difference between a quit and a
/// wedge. v1's `force` waived the whole front gate, and v1 gated on the UNSAVED
/// count — so on 2026-09-08, with five of Drew's clean documents open and
/// `unsavedDocuments:0`, a bare `swx_stop` would have sailed past the guard and
/// tried to quit a session full of his work.
#[allow(clippy::too_many_arguments)]
/// DOES THE SESSION WE ARE ABOUT TO END MATCH THE ONE THE LAUNCH RECORD CLAIMS?
///
/// Belt and braces, added after a re-attribution bug labelled a session
/// `attributed:true` while COM was attached to a DIFFERENT process: the ladder
/// builds its `Identity` from the COM pid, and a wrong attribution record would
/// then have authorised ending a process nothing had proved was ours.
///
/// `claimed` is the launch's own record, `identity` is what COM answers for now.
/// A mismatch refuses. No claim at all (`None`) is not a mismatch — it is simply
/// a check with nothing to compare, and the other preconditions still stand.
pub fn identity_matches_attribution(
claimed: Option<(u32, u64)>,
identity: Option<&Identity>,
) -> Result<(), Refusal> {
let (Some((pid, created)), Some(id)) = (claimed, identity) else { return Ok(()) };
if id.pid == pid && id.created_at == created {
return Ok(());
}
Err(Refusal::Known {
code: "identity_mismatch",
message: format!(
"this bridge's launch record claims SOLIDWORKS pid {pid} (created {created}), but COM \
is attached to pid {} (created {}) — so the session about to be ended is NOT the one \
the attribution was proved for, and nothing here may act on that difference",
id.pid, id.created_at
),
})
}
pub fn check_preconditions(
open_docs: Option<u64>,
listed_documents: Option<u64>,
docs_truncated: bool,
unsaved: Option<u64>,
dialog: Option<&BlockingDialog>,
dialog_census_readable: bool,
busy: Option<bool>,
host_busy: bool,
responding: Option<bool>,
identity: Option<&Identity>,
launch_in_flight: bool,
force: bool,
) -> Result<(), Refusal> {
// ORDER MATTERS. The checks that do NOT depend on being able to identify the
// process come first, because a fact that is DEFINITELY TRUE is more useful
// to a caller than an unknown: "six documents are open" is actionable, while
// "I could not read the pid" masks it. Every one of these still refuses.
//
// P1 — the OPEN count, and only from a count the enumeration agreed with.
if docs_truncated {
return Err(Refusal::Unknown {
what: "how many documents are open",
detail: "the document set is too large to enumerate (likely leaked import temps), so \
an empty list would mean 'unlistable', not 'none open'. Run \
swx_close_import_temps, then retry."
.into(),
});
}
match open_docs {
None => {
return Err(Refusal::Unknown {
what: "how many documents are open",
detail: "GetDocumentCount did not answer. Quitting with the user's documents open \
is the one outcome this verb exists to prevent."
.into(),
})
}
Some(0) => {}
Some(n) => {
return Err(Refusal::Known {
code: "open_documents_present",
message: format!(
"{n} document(s) are open in the user's SOLIDWORKS — this bridge quits only a \
session with NONE open, and `force` does not waive that"
),
})
}
}
// P1b — THE COUNT MUST AGREE WITH THE ENUMERATION. `GetDocumentCount` and
// `GetDocuments` are two different COM reads, and the Python gate has always
// cross-checked them before it would let a live run proceed; the verb did
// not. When they disagree neither is evidence, and "zero" is precisely the
// answer we are least entitled to assume. (Above the cap the enumeration is
// deliberately empty — that case is already refused above as truncated.)
match (open_docs, listed_documents) {
(Some(c), Some(l)) if c != l => {
return Err(Refusal::Unknown {
what: "how many documents are open",
detail: format!(
"GetDocumentCount says {c} and the enumeration lists {l} — they DISAGREE, so \
neither is evidence and 'none open' is not established"
),
})
}
_ => {}
}
// P2 — the unsaved count. The one thing `force` may waive.
if !force {
match unsaved {
None => {
return Err(Refusal::Unknown {
what: "whether any open document has unsaved changes",
detail: "GetSaveFlag did not answer for every document. An unanswerable \
question is not permission."
.into(),
})
}
Some(0) => {}
Some(n) => {
return Err(Refusal::Known {
code: "unsaved_documents",
message: format!("{n} open document(s) have unsaved changes"),
})
}
}
}
// P7 — the identity. Everything below this line is measured against a
// specific process, and without a pid AND its creation time "the process
// went away" cannot be told from "a pid was reused".
if identity.is_none() {
return Err(Refusal::Unknown {
what: "which SOLIDWORKS process this is (pid + creation time + main window)",
detail: "GetProcessID / the process census / GetHWndx64 did not all answer. Every \
rung's success measurement is 'this exact process is gone', which a pid \
without a creation time cannot express."
.into(),
});
}
// P3 — no blocking dialog, from a census we could actually take.
if !dialog_census_readable {
return Err(Refusal::Unknown {
what: "whether a dialog is on screen",
detail: "the window census could not be taken. A quit sent at a modal is a different \
act with a different outcome — on 2026-09-02 closing a stuck SOLIDWORKS \
modal took the whole application down."
.into(),
});
}
if let Some(d) = dialog {
return Err(Refusal::Known {
code: d.error_code(),
message: format!(
"SOLIDWORKS is showing a dialog ({}): \"{}\" (hwnd {}, class {}, owned by {}) — this bridge never answers a \
dialog, so the quit stops here",
d.kind,
d.title,
d.hwnd,
d.window.as_ref().map(|w| w.class_name.as_str()).unwrap_or("?"),
d.window.as_ref().and_then(|w| w.owner).map(|o| o.to_string()).unwrap_or_else(|| "no window".into())
),
});
}
// P4 — not busy, on both sides.
match busy {
None => {
return Err(Refusal::Unknown {
what: "whether SOLIDWORKS is mid-command",
detail: "CommandInProgress did not answer.".into(),
})
}
Some(true) => {
return Err(Refusal::Known {
code: "host_app_busy",
message: "SOLIDWORKS is in the middle of a command".into(),
})
}
Some(false) => {}
}
if host_busy {
return Err(Refusal::Known {
code: "host_app_busy",
message: "this bridge's own com host is busy with another verb".into(),
});
}
// P5 — responding.
match responding {
None => {
return Err(Refusal::Unknown {
what: "whether the SOLIDWORKS window is responding",
detail: "IsHungAppWindow could not be read for the main frame.".into(),
})
}
Some(false) => {
return Err(Refusal::Known {
code: "host_app_hung",
message: "the SOLIDWORKS window is not responding — a quit request cannot be \
delivered to a message loop that is not running"
.into(),
})
}
Some(true) => {}
}
// A launch we started is still in flight: quitting into it would race our
// own supervisor and leave a half-started session nobody owns.
if launch_in_flight {
return Err(Refusal::Known {
code: "launch_in_flight",
message: "this bridge is still launching SOLIDWORKS — poll swx_status until the \
launch finishes before asking it to quit"
.into(),
});
}
Ok(())
}
/// The hint when a DIALOG stopped the ladder. Different from a decline in the
/// way that matters: the app is not refusing, it is ASKING — and the answer is
/// the user's to give.
pub fn blocked_hint(d: &BlockingDialog) -> String {
let common = "This bridge never answers a dialog. Closing a stuck SOLIDWORKS modal through \
the window manager took the whole application down on 2026-09-02, and the \
ladder stops at the first new window for exactly that reason — nothing was \
clicked, nothing was Escaped, and no further rung was tried. swx_state takes a \
BACKGROUND screenshot (it steals no focus) so you can show the user what is on \
screen. ANSWERING IT IS THEIR DECISION.";
let title = &d.title;
match d.kind {
"save_changes" => format!(
"SOLIDWORKS IS ASKING WHETHER TO SAVE: {title:?}. At a VERIFIED zero open documents \
this dialog should be impossible, so its appearance means the document count this \
bridge read was WRONG — a bug worth reporting, not a dialog to dismiss. The buttons \
offer to write the user's work to This PC or to 3DEXPERIENCE, or to discard it. {common}"
),
"license" => format!(
"SOLIDWORKS IS ASKING ABOUT THE 3DEXPERIENCE SESSION OR LICENCE: {title:?}. A \
sign-out affects the user's whole platform session, not just this application, and \
this pass found no vendor documentation of a safe default — so there is no safe \
choice for the bridge to make on their behalf. {common}"
),
"document_recovery" => format!(
"SOLIDWORKS IS SHOWING DOCUMENT RECOVERY: {title:?}. The last session ended badly and \
there is unrecovered work behind that dialog. {common} swx_check_recovery describes it."
),
_ => format!(
"SOLIDWORKS RAISED A WINDOW THIS BRIDGE DOES NOT RECOGNISE: {title:?}. An \
unanswerable question is not permission. {common}"
),
}
}
/// The hint on a declined quit. It names what a HUMAN should click, and it must
/// never mention `swx_kill`.
pub fn declined_hint(rungs_tried: usize, max_rung: &str) -> String {
format!(
"SOLIDWORKS DID NOT QUIT, and nothing was forced. {rungs_tried} rung(s) up to '{max_rung}' \
were tried and each one's measurement is in `rungsTried` — read `result` and \
`processGone` per rung rather than the summary. WHAT A HUMAN SHOULD DO: ask the user to \
click File ▸ Exit (or the window's X) in SOLIDWORKS themselves; that gesture works when \
these do not, and it is theirs to make. If the ladder was pinned (`maxRung` defaults to \
\"{}\" until the live quit gate has run), a higher rung may work — see \
docs/QUIT-INVESTIGATION.md §5.3. Do NOT reach for a force-kill: on a Connected edition a \
killed session leaves platform objects locked to the user, un-lockable except by hand, \
and orphans the per-session children.",
DEFAULT_MAX_RUNG
)
}
#[cfg(test)]
mod tests {
use super::*;
fn id() -> Identity {
Identity { pid: 107688, created_at: 1_788_899_741, main_hwnd: Some(6_622_700) }
}
fn proc(pid: u32, image: &str, created: Option<u64>) -> ProcInfo {
ProcInfo { pid, image: image.into(), created_at: created, ppid: None }
}
fn win(hwnd: i64, class: &str, title: &str) -> WindowInfo {
WindowInfo { hwnd, class_name: class.into(), title: title.into(), visible: true, ..Default::default() }
}
/// WHICH WINDOWS BLOCK — a cases table of measured evidence → verdict (0.9.4).
/// Each row says which of its fields were MEASURED on the live machine.
#[test]
fn windows_are_judged_by_measured_modality() {
use crate::launchos::DialogContents;
let contents = |controls: &[(&str, bool)], text: &[&str], buttons: &[&str]| DialogContents {
text: text.iter().map(|t| t.to_string()).collect(),
buttons: buttons.iter().map(|t| t.to_string()).collect(),
controls: controls.iter().map(|(c, v)| (c.to_string(), *v)).collect(),
..Default::default()
};
let window = |title: &str, owner_enabled: Option<bool>, exstyle: Option<u32>| WindowInfo {
hwnd: 331370,
class_name: "#32770".into(),
title: title.into(),
visible: true,
owner: Some(4242),
owner_enabled,
style: Some(0x9400_0044),
exstyle,
rect: Some([533, 45, 834, 112]),
};
// THE CANONICAL NOTIFICATION — every field MEASURED 2026-10-01 on Drew's
// machine: hwnd 331370, owner = the main frame, owner ENABLED for all 20
// reads over 95 s, exstyle 0x90080 (TOOLWINDOW | LAYERED | CONTROLPARENT),
// 301x67 px at the top of the frame, ALL children Static, the visible ones
// "Title"/"Description" and the hidden ones "Button1"/"Button2"/
// "Hyperlink Text 1"/"Hyperlink Text 2" — template placeholders: the
// window showed "Connected to OI000616940 — SOLIDWORKS is connected to
// your 3DEXPERIENCE platform" (known as 3dexperience_connected; that name
// is documentation, not a rule).
let connected = contents(
&[("Static", true), ("Static", true), ("Static", false), ("Static", false), ("Static", false), ("Static", false)],
&["Title", "Description", "Button1", "Button2", "Hyperlink Text 1", "Hyperlink Text 2"],
&[],
);
// THE LICENCE DIALOG — MEASURED 2026-09-17: its title "License Manager",
// its text and its one button (OK). NOT measured: whether its owner frame
// was disabled. It blocks BY KIND, whatever the owner says.
let license = contents(&[("Static", true), ("Button", true)], &["Failed to check 1 license(s): …"], &["OK"]);
let with_button = contents(&[("Static", true), ("Button", true)], &["Continue?"], &["OK"]);
let modal_statics = contents(&[("Static", true)], &["Working…"], &[]);
let custom_save = contents(
&[("Static", true), ("Static", true), ("Static", true), ("Static", true)],
&["Do you want to save changes to Part1.SLDPRT?", "Save", "Don't Save", "Cancel"],
&[],
);
let two_choices = contents(&[("Static", true), ("Static", false), ("Static", false)], &["Proceed?", "&Yes", "No"], &[]);
let recovered_text = contents(&[("Static", true)], &["SOLIDWORKS recovered 2 documents"], &[]);
let one_close = contents(&[("Static", true), ("Static", true)], &["Connected", "Close"], &[]);
type Row<'a> = (&'a str, &'a str, Option<bool>, Option<u32>, Option<&'a DialogContents>, bool, &'a str);
let rows: Vec<Row> = vec![
("the 3DEXPERIENCE connected notice (measured)", "", Some(true), Some(0x90080), Some(&connected), false, "notification"),
("the same window, owner frame DISABLED", "", Some(false), Some(0x90080), Some(&connected), true, "unknown"),
("the same window, owner enablement UNREAD", "", None, Some(0x90080), Some(&connected), true, "unknown"),
("the same window, controls UNREADABLE", "", Some(true), Some(0x90080), None, true, "unknown"),
("the same window, extended style UNREAD", "", Some(true), None, Some(&connected), true, "unknown"),
("the same window, NOT a tool window", "", Some(true), Some(0x10000), Some(&connected), true, "unknown"),
("an unknown window with a Button, owner enabled", "", Some(true), Some(0x90080),
Some(&with_button), true, "unknown"),
("an unknown MODAL: owner disabled, statics only", "", Some(false), Some(0x10000),
Some(&modal_statics), true, "unknown"),
("the licence dialog (blocking by kind; owner not measured)", "License Manager", None, None, Some(&license), true, "license"),
("the licence dialog even with an enabled owner", "License Manager", Some(true), Some(0x90080), Some(&license), true, "license"),
("a save prompt, by kind", "Save Modified Documents", Some(true), Some(0x90080), Some(&connected), true, "save_changes"),
// 0.9.5, adversary pass 43 — the window's WORDS (not measured live:
// constructed from the measured notice's evidence).
("a custom-drawn save prompt: notice evidence, the question in its text", "SOLIDWORKS", Some(true), Some(0x90080),
Some(&custom_save), true, "save_changes"),
("a custom-drawn choice: notice evidence, two button-like Static captions", "", Some(true), Some(0x90080),
Some(&two_choices), true, "unknown"),
("ONE button-like caption is not a choice", "", Some(true), Some(0x90080), Some(&one_close), false, "notification"),
("the measured notice WITH a licence-type title: evidence beats a title-only guess", "3DEXPERIENCE Platform",
Some(true), Some(0x90080), Some(&connected), false, "notification"),
("...and the same title on a modal still blocks, as license", "3DEXPERIENCE Platform", Some(false), Some(0x90080),
Some(&connected), true, "license"),
("a recovery prompt in its TEXT, notice evidence", "", Some(true), Some(0x90080),
Some(&recovered_text), true, "document_recovery"),
("a recovery title is never overridden by evidence", "Document Recovery", Some(true), Some(0x90080),
Some(&connected), true, "document_recovery"),
("an import-diagnostics title is never overridden by evidence", "Import Diagnostics", Some(true), Some(0x90080),
Some(&connected), true, "import_diagnostics"),
];
for (label, title, owner_enabled, exstyle, c, want_blocking, want_kind) in rows {
let (blocking, kind, why) = judge_window(&window(title, owner_enabled, exstyle), c);
assert_eq!((blocking, kind), (want_blocking, want_kind), "{label}: {why}");
}
assert!(text_is_template_placeholder(&connected), "the template's captions are not the window's words");
assert!(!text_is_template_placeholder(&license));
// The measured placeholders are NOT button-like: the notice stays a notice.
assert!(button_like_captions(&connected).is_empty(), "template placeholders are not choices");
assert_eq!(button_like_captions(&custom_save), vec!["Save", "Don't Save", "Cancel"]);
}
/// The signature that matters: everything measured, nothing waived.
fn ok_preconditions(force: bool) -> Result<(), Refusal> {
check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()),
false, force,
)
}
#[test]
fn a_session_with_open_documents_is_never_quit_even_with_force() {
// THE REGRESSION TEST FOR THE LIVE POSTURE. On 2026-09-08 five of Drew's
// clean documents were open with unsavedDocuments:0, and v1 gated on the
// UNSAVED count — so a bare swx_stop would have tried to quit them.
for force in [false, true] {
let r = check_preconditions(
Some(5), Some(5), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()),
false, force,
);
assert_eq!(
r.as_ref().err().map(|e| e.code()),
Some("open_documents_present"),
"5 open documents must refuse (force={force}): {r:?}"
);
}
}
#[test]
fn force_waives_the_unsaved_question_and_nothing_else() {
// P2 only.
assert!(check_preconditions(
Some(0), Some(0), false, Some(3), None, true, Some(false), false, Some(true), Some(&id()),
false, true
)
.is_ok());
assert_eq!(
check_preconditions(
Some(0), Some(0), false, Some(3), None, true, Some(false), false, Some(true), Some(&id()),
false, false
)
.err()
.map(|e| e.code()),
Some("unsaved_documents")
);
// ...and force waives NONE of the others.
let dialog = BlockingDialog { hwnd: 9, title: "Save changes?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
let cases: Vec<(&str, Result<(), Refusal>)> = vec![
("open docs", check_preconditions(
Some(2), Some(2), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, true)),
("truncated", check_preconditions(
None, None, true, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, true)),
("dialog", check_preconditions(
Some(0), Some(0), false, Some(0), Some(&dialog), true, Some(false), false, Some(true), Some(&id()), false, true)),
("census unreadable", check_preconditions(
Some(0), Some(0), false, Some(0), None, false, Some(false), false, Some(true), Some(&id()), false, true)),
("busy", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(true), false, Some(true), Some(&id()), false, true)),
("host busy", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), true, Some(true), Some(&id()), false, true)),
("hung", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(false), Some(&id()), false, true)),
("no identity", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), None, false, true)),
("launch in flight", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), true, true)),
];
for (what, r) in cases {
assert!(r.is_err(), "force must not waive {what}");
}
}
#[test]
fn every_unknown_refuses_with_one_branchable_code() {
// A caller's move is the same for all of them, and it is never "proceed".
let unknowns: Vec<Result<(), Refusal>> = vec![
check_preconditions(
None, None, false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, false),
check_preconditions(
None, None, true, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, false),
check_preconditions(
Some(0), Some(0), false, None, None, true, Some(false), false, Some(true), Some(&id()), false, false),
check_preconditions(
Some(0), Some(0), false, Some(0), None, false, Some(false), false, Some(true), Some(&id()), false, false),
check_preconditions(
Some(0), Some(0), false, Some(0), None, true, None, false, Some(true), Some(&id()), false, false),
check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, None, Some(&id()), false, false),
check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), None, false, false),
];
for r in unknowns {
assert_eq!(r.err().map(|e| e.code()), Some("stop_preconditions_unknown"));
}
assert!(ok_preconditions(false).is_ok(), "a fully-measured clean session may proceed");
}
#[test]
fn a_save_prompt_gets_its_own_code_because_it_should_be_impossible() {
let save = BlockingDialog { hwnd: 9, title: "Save changes to Part1?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
assert_eq!(save.error_code(), "stop_blocked_by_save_prompt");
let other = BlockingDialog { hwnd: 9, title: "3DEXPERIENCE sign in".into(), kind: "license", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
assert_eq!(other.error_code(), "stop_blocked_by_dialog");
// ...and each stops the ladder from the precondition sweep.
for d in [&save, &other] {
let r = check_preconditions(
Some(0), Some(0), false, Some(0), Some(d), true, Some(false), false, Some(true), Some(&id()),
false, true,
);
assert_eq!(r.err().map(|e| e.code()), Some(d.error_code()));
}
}
#[test]
fn the_dialog_census_finds_a_modal_and_ignores_the_frame() {
let windows = vec![
win(6_622_700, "Afx:00007FF6", "SOLIDWORKS Design"),
win(9001, "#32770", "Save Modified Documents"),
];
let d = first_blocking_dialog(&windows).expect("the modal is found");
assert_eq!(d.hwnd, 9001);
assert_eq!(d.kind, "save_changes");
// The main frame alone is NOT a dialog.
assert!(first_blocking_dialog(&windows[..1]).is_none());
// An invisible dialog is not on screen.
let mut hidden = win(9002, "#32770", "Save changes?");
hidden.visible = false;
assert!(first_blocking_dialog(&[hidden]).is_none());
}
#[test]
fn process_gone_needs_the_creation_time_and_an_unreadable_census_is_unknown() {
let alive = vec![proc(107688, "SLDWORKS.exe", Some(1_788_899_741))];
assert_eq!(process_gone(Ok(&alive), &id()), Some(false));
// Same pid, DIFFERENT creation time: our session is gone and something
// else has the number.
let reused = vec![proc(107688, "SLDWORKS.exe", Some(1_788_999_999))];
assert_eq!(process_gone(Ok(&reused), &id()), Some(true));
let empty: Vec<ProcInfo> = Vec::new();
assert_eq!(process_gone(Ok(&empty), &id()), Some(true));
// UNREADABLE: a rung that cannot be measured is a rung that failed.
let err = "census failed".to_string();
assert_eq!(process_gone(Err(&err), &id()), None);
}
#[test]
fn the_expected_survivors_are_not_treated_as_a_failure() {
// The pid is gone, the children are reaped, and the platform daemons are
// still alive — which is a CLEAN quit, by design (§2.6).
let census = vec![
proc(6076, "3DEXPERIENCELauncher.exe", Some(1)),
proc(90552, "3DEXPERIENCELauncherSysTray.exe", Some(1)),
proc(700, "EdmServerV6.exe", Some(1)),
proc(701, "ENOPLMCSAClient.exe", Some(1)),
proc(9, "ViewServerV6.exe", Some(1)),
];
assert_eq!(process_gone(Ok(&census), &id()), Some(true));
let (children, survivors, unjudged) = reap_report(&census);
assert!(children.is_empty(), "no per-session child survived: {children:?}");
assert_eq!(survivors.len(), 4, "the daemons are alive and that is consent: {survivors:?}");
assert_eq!(unjudged, vec!["ViewServerV6.exe"], "recorded, not judged");
}
#[test]
fn the_ladder_order_is_fixed_and_pinned_at_a_by_default() {
assert_eq!(RUNGS, &["a", "b", "c", "d", "e", "f"]);
assert_eq!(DEFAULT_MAX_RUNG, "a", "shipped pinned until the live gate has run");
assert_eq!(rungs_up_to("a"), vec!["a"]);
assert_eq!(rungs_up_to("c"), vec!["a", "b", "c"]);
assert_eq!(rungs_up_to("f").len(), 6);
assert!(rungs_up_to("g").is_empty(), "an unknown max yields nothing to try");
assert!(!is_rung("g"));
// There is no rung (g), and no rung mentions a kill.
for r in RUNGS {
let a = rung_action(r).to_lowercase();
for forbidden in ["kill", "terminate"] {
assert!(!a.contains(forbidden), "rung {r} action mentions '{forbidden}': {a}");
}
// `force` may appear ONLY as the thing being omitted.
if a.contains("force") {
assert!(
a.contains("omitted"),
"rung {r} mentions force without saying it is omitted: {a}"
);
}
assert!(rung_budget_secs(r) >= 30);
}
assert!(rung_action("f").contains("OMITTED"), "the Restart Manager rung says what it omits");
}
#[test]
fn a_result_the_host_does_not_explain_is_unmeasured_not_declined() {
assert_eq!(result_from_host("exited"), RungResult::Quit);
assert_eq!(result_from_host("declined"), RungResult::Declined);
assert_eq!(result_from_host("uipiBlocked"), RungResult::UipiBlocked);
assert_eq!(result_from_host("dialogAppeared"), RungResult::DialogAppeared);
assert_eq!(result_from_host("skipped"), RungResult::Skipped);
assert_eq!(result_from_host("something new"), RungResult::Unmeasured);
// Which of them stop the ladder.
assert!(RungResult::Quit.stops_ladder());
assert!(RungResult::DialogAppeared.stops_ladder());
assert!(RungResult::UipiBlocked.stops_ladder());
assert!(!RungResult::Declined.stops_ladder());
assert!(!RungResult::Skipped.stops_ladder());
assert!(!RungResult::Unmeasured.stops_ladder());
}
#[test]
fn a_count_that_disagrees_with_the_enumeration_is_unknown_not_zero() {
// GetDocumentCount and GetDocuments are two different COM reads. The
// Python gate has always cross-checked them; the verb did not, so a
// count of 0 beside a list of 1 was "nothing open, go ahead".
let r = check_preconditions(
Some(0), Some(1), false, Some(0), None, true, Some(false), false, Some(true),
Some(&id()), false, true,
);
assert_eq!(r.as_ref().err().map(|e| e.code()), Some("stop_preconditions_unknown"), "{r:?}");
assert!(r.unwrap_err().message().contains("DISAGREE"));
// Agreement, and an absent enumeration, both proceed — above the cap the
// truncated refusal has already fired.
assert!(check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true),
Some(&id()), false, true
)
.is_ok());
assert!(check_preconditions(
Some(0), None, false, Some(0), None, true, Some(false), false, Some(true),
Some(&id()), false, true
)
.is_ok());
}
#[test]
fn the_dialog_census_covers_the_cef_children_and_only_ours() {
// A 3DEXPERIENCE sign-in window is a web view: its hwnd belongs to
// swCefSubProc.exe, a CHILD of sldworks.exe. Censusing the SOLIDWORKS pid
// alone proved "no dialog" while the platform held a modal.
let ours = 107688u32;
let census = vec![
proc(ours, "SLDWORKS.exe", Some(1000)),
ProcInfo { pid: 501, image: "swCefSubProc.exe".into(), created_at: Some(1001), ppid: Some(ours) },
ProcInfo { pid: 502, image: "swCefSubProc.exe".into(), created_at: Some(1002), ppid: Some(ours) },
// ANOTHER session's web view — never ours to judge or to send to.
ProcInfo { pid: 777, image: "swCefSubProc.exe".into(), created_at: Some(1003), ppid: Some(999) },
proc(600, "sldProcMon.exe", Some(1004)),
];
let pids = dialog_census_pids(Ok(&census), &id()).expect("a readable census");
assert_eq!(pids, vec![ours, 501, 502], "sldworks + its OWN cef children: {pids:?}");
// An unreadable census is UNKNOWN, which refuses — never an empty list,
// which would read as "no windows to worry about".
let e = "census unreadable".to_string();
assert!(dialog_census_pids(Err(&e), &id()).is_none());
}
/// A WRONG ATTRIBUTION RECORD MUST NOT BE ABLE TO END A DIFFERENT SESSION.
/// The ladder builds its Identity from the COM pid; the launch record is a
/// claim about one process. If they name different processes, the claim does
/// not cover the act.
#[test]
fn the_launch_claim_must_be_about_the_session_com_answers_for() {
let me = id();
// No claim at all: nothing to compare, and the other preconditions stand.
assert!(identity_matches_attribution(None, Some(&me)).is_ok());
// The claim IS about this session.
assert!(identity_matches_attribution(Some((me.pid, me.created_at)), Some(&me)).is_ok());
// A different pid, or the same pid with a different creation time: refuse.
for claim in [(me.pid + 1, me.created_at), (me.pid, me.created_at + 1)] {
let r = identity_matches_attribution(Some(claim), Some(&me));
assert_eq!(
r.as_ref().err().map(|e| e.code()),
Some("identity_mismatch"),
"a claim about {claim:?} must not authorise acting on {me:?}"
);
}
}
#[test]
fn a_recycled_window_handle_is_never_sent_to() {
let me = id();
// The same window: pid AND creation time agree.
assert!(hwnd_still_owned(Some(me.pid), Some(me.created_at), &me));
// The pid was REUSED: same number, different process.
assert!(!hwnd_still_owned(Some(me.pid), Some(me.created_at + 5), &me));
// Somebody else's window on our handle.
assert!(!hwnd_still_owned(Some(me.pid + 1), Some(me.created_at), &me));
// Either fact unreadable is a NO. Rungs (c)/(d) skip rather than send.
assert!(!hwnd_still_owned(None, Some(me.created_at), &me));
assert!(!hwnd_still_owned(Some(me.pid), None, &me));
assert!(!hwnd_still_owned(None, None, &me));
}
#[test]
fn the_declined_hint_never_recommends_a_kill() {
let h = declined_hint(3, "c").to_lowercase();
assert!(h.contains("file ▸ exit") || h.contains("file "), "it names the human gesture: {h}");
assert!(!h.contains("swx_kill"), "never recommends the kill verb: {h}");
assert!(h.contains("do not reach for a force-kill"));
}
}
/// The shipped exit macro, if it is there.
///
/// Rung (e) runs `RunMacro2` on a two-line VBA project
/// (`Sub main() : Application.SldWorks.ExitApp : End Sub`) — the only rung whose
/// request comes from INSIDE the process being asked to quit, where no external
/// client's reference count applies. It is deliberately not generated at
/// runtime: it is a reviewed file, matched by content hash before use.
///
/// A `.swp` is a binary VBA project that cannot be authored without SOLIDWORKS,
/// so this repository does not ship one yet and the rung reports `skipped` until
/// it does. That is the honest state: a rung that cannot run says so, rather
/// than being quietly absent from the ladder.
pub const EXIT_MACRO_NAME: &str = "swx-exit.swp";
/// SHA-256 of the reviewed macro. `None` until the file is generated and
/// reviewed on a machine with SOLIDWORKS; the rung refuses to run an unhashed
/// or mismatched file.
pub const EXIT_MACRO_SHA256: Option<&str> = None;
pub fn exit_macro_path() -> Option<String> {
let exe = std::env::current_exe().ok()?;
let p = exe.parent()?.join("resources").join("macros").join(EXIT_MACRO_NAME);
if !p.is_file() {
return None;
}
// A hash we have not recorded yet cannot be matched, so the rung stays
// unavailable rather than running an unreviewed macro in somebody's session.
EXIT_MACRO_SHA256?;
Some(p.to_string_lossy().to_string())
}
#[cfg(windows)]
pub mod win {
//! The window-message and Restart-Manager rungs.
//!
//! These live in the com host rather than being delegated to ab, for one
//! reason: ab's close is a UIA `WindowPattern.Close`, which is the mechanism
//! that took the whole application down on 2026-09-02, and it is unavailable
//! whenever ab cannot be reached (`abAvailable:false` on the dev bridge).
//! `SendMessageTimeout` / `PostMessage` are twenty lines here, synchronous,
//! timeout-bounded and measurable.
use windows::Win32::Foundation::{GetLastError, SetLastError, HWND, LPARAM, WPARAM, ERROR_ACCESS_DENIED, WIN32_ERROR};
use windows::Win32::UI::WindowsAndMessaging::{
GetWindowThreadProcessId, PostMessageW, SendMessageTimeoutW, SMTO_ABORTIFHUNG, SMTO_NORMAL,
WM_CLOSE, WM_SYSCOMMAND,
};
/// WHOSE WINDOW IS THIS, RIGHT NOW? `None` when the handle names no window
/// any more.
///
/// Handles are recycled. The hwnd a message rung sends to was captured at
/// precondition time, and the rung before it exists to make that window go
/// away — so between capture and send the handle can belong to somebody
/// else's window, and `WM_CLOSE` to somebody else's window is a close nobody
/// asked for.
pub fn window_pid(h: i64) -> Option<u32> {
let mut pid: u32 = 0;
let tid = unsafe { GetWindowThreadProcessId(hwnd(h), Some(&mut pid)) };
if tid == 0 || pid == 0 {
None
} else {
Some(pid)
}
}
/// `SC_CLOSE` — the system-menu Close.
const SC_CLOSE: usize = 0xF060;
fn hwnd(h: i64) -> HWND {
HWND(h as *mut std::ffi::c_void)
}
/// UIPI FAILURES RETURN SUCCESS. `SendMessage`/`PostMessage` to a
/// higher-integrity window fail with `ERROR_ACCESS_DENIED` and a return value
/// indistinguishable from success, so `SetLastError(0)` first and read
/// `GetLastError()` after — an unchecked one reports a quit that never
/// happened (docs/QUIT-INVESTIGATION.md §2.3).
fn blocked_by_uipi() -> bool {
let last = unsafe { GetLastError() };
last == ERROR_ACCESS_DENIED
}
/// Rung (c): what clicking X does, and on MFC literally what File ▸ Exit
/// does. Timeout-bounded so a modal cannot wedge the com host.
pub fn send_wm_close(h: i64) -> (bool, bool, String) {
unsafe { SetLastError(WIN32_ERROR(0)) };
let mut out: usize = 0;
let r = unsafe {
SendMessageTimeoutW(
hwnd(h),
WM_CLOSE,
WPARAM(0),
LPARAM(0),
SMTO_ABORTIFHUNG | SMTO_NORMAL,
5_000,
Some(&mut out as *mut usize),
)
};
let blocked = blocked_by_uipi();
let ok = r.0 != 0;
(
ok && !blocked,
blocked,
format!(
"SendMessageTimeout(WM_CLOSE) -> hwnd {h}: returned {}, result {out}{}",
r.0,
if blocked { ", ERROR_ACCESS_DENIED (UIPI)" } else { "" }
),
)
}
/// Rung (d): the system-menu Close. Posted, so it cannot block, and it
/// enters through `OnSysCommand` before `DefWindowProc` turns it into a
/// `WM_CLOSE` — an app that filters one may honour the other.
pub fn post_sc_close(h: i64) -> (bool, bool, String) {
unsafe { SetLastError(WIN32_ERROR(0)) };
let posted =
unsafe { PostMessageW(Some(hwnd(h)), WM_SYSCOMMAND, WPARAM(SC_CLOSE), LPARAM(0)) };
let blocked = blocked_by_uipi();
let ok = posted.is_ok();
(
ok && !blocked,
blocked,
format!(
"PostMessage(WM_SYSCOMMAND, SC_CLOSE) -> hwnd {h}: {}{}",
if ok { "posted" } else { "refused" },
if blocked { ", ERROR_ACCESS_DENIED (UIPI)" } else { "" }
),
)
}
/// Rung (f): the OS's own POLITE shutdown, and the only route that takes the
/// pid **and its creation time** — the same identity pair the attribution law
/// demands.
///
/// **`RmForceShutdown` IS NOT REACHABLE FROM THIS CODE.** The flags argument
/// is a literal `0`, not a parameter and not a runtime option: with the force
/// flag omitted `RmShutdown` never terminates anything, and a refusal comes
/// back as `ERROR_FAIL_SHUTDOWN (351)` with a readable per-process status.
/// It sends `WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP)` — which means "save
/// automatically without prompting" — which is why this rung is LAST.
pub fn restart_manager_shutdown(pid: u32, created_at_epoch: u64) -> (bool, String) {
use windows::core::PWSTR;
use windows::Win32::Foundation::{FILETIME, ERROR_SUCCESS};
use windows::Win32::System::RestartManager::{
RmEndSession, RmRegisterResources, RmShutdown, RmStartSession, RM_UNIQUE_PROCESS,
CCH_RM_SESSION_KEY,
};
let mut session: u32 = 0;
// CCH_RM_SESSION_KEY wide chars plus the terminator.
let mut key = vec![0u16; CCH_RM_SESSION_KEY as usize + 1];
let started =
unsafe { RmStartSession(&mut session, None, PWSTR(key.as_mut_ptr())) };
if started != ERROR_SUCCESS {
return (false, format!("RmStartSession failed: {}", started.0));
}
// epoch seconds -> FILETIME (100ns ticks since 1601-01-01).
let ticks = (created_at_epoch + 11_644_473_600) * 10_000_000;
let ft = FILETIME {
dwLowDateTime: (ticks & 0xFFFF_FFFF) as u32,
dwHighDateTime: (ticks >> 32) as u32,
};
let target = [RM_UNIQUE_PROCESS { dwProcessId: pid, ProcessStartTime: ft }];
let reg = unsafe { RmRegisterResources(session, None, Some(&target), None) };
if reg != ERROR_SUCCESS {
unsafe {
let _ = RmEndSession(session);
}
return (false, format!("RmRegisterResources failed: {}", reg.0));
}
// ═══ FLAGS = 0. `RmForceShutdown` IS NOT WRITTEN ANYWHERE IN THIS FILE.
// Not a parameter, not a runtime option, not a named constant that could
// be passed by mistake — a literal zero. With the force flag omitted
// RmShutdown NEVER terminates anything: it sends
// WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP) then WM_ENDSESSION (and a
// WM_CLOSE to GUI apps that ignore those), and a refusal comes back as
// ERROR_FAIL_SHUTDOWN (351) rather than a dead process.
let shutdown = unsafe { RmShutdown(session, 0, None) };
unsafe {
let _ = RmEndSession(session);
}
let ok = shutdown == ERROR_SUCCESS;
let note = if ok {
format!("RmShutdown(pid {pid}, force OMITTED) returned success")
} else {
format!(
"RmShutdown(pid {pid}, force OMITTED) refused: Win32 error {} \
(351 = ERROR_FAIL_SHUTDOWN, meaning the app declined the OS's own polite request; \
nothing was terminated)",
shutdown.0
)
};
(ok, note)
}
}