app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408
//! LAUNCH RESILIENCE — the state machine that starts SOLIDWORKS and recovers
//! when the 3DEXPERIENCE launcher chain stalls.
//!
//! ## What went wrong, measured
//!
//! On 2026-09-04, five of five launches this bridge made produced a
//! `SWXDesktopLauncher` log that stopped at `SecurityContext:` and went no
//! further: no `Run Begin`, no helper log, no `sldworks.exe`. `CATSTART.exe` and
//! `SWXDesktopLauncher.exe` sat idle at ~50 MB forever. Every launch of the SAME
//! shortcut with the SAME arguments from an INTERACTIVE context — a Start-Menu
//! click, the crash reporter's "Restart SOLIDWORKS Design" button — reached
//! `Run Begin` about two seconds later and started SOLIDWORKS.
//!
//! The difference is the startup context. The bridge ran the shortcut through
//! `cmd.exe /c start` with `DETACHED_PROCESS | CREATE_NO_WINDOW`, and the
//! launcher's Run thread never started. That is a HYPOTHESIS, not a proven
//! cause, and this module is written so it does not need to be right: it tries
//! routes in order, DETECTS the stall from the launcher's own log, cleans up
//! after itself, and moves to the next one — then remembers which route worked
//! so the next launch starts there.
//!
//! ## The four failures this module answers
//!
//! | # | Failure | Answer here |
//! |---|---|---|
//! | A | the chain stalls before `Run Begin` | [`Supervisor`] watches the log, kills ONLY the chain it started, tries the next route |
//! | B | a second `swx_start` fires the launcher again | the attempt is persisted; a call during flight NEVER launches (`stillLaunching`) |
//! | C | SOLIDWORKS crashes on first spawn and the reporter appears | [`CRASH_RESTART_BUTTON`] is clicked ONCE, ledgered, and never anything else |
//! | D | SOLIDWORKS is up but hung or memory-exhausted | [`host_app_hung`] / [`host_app_exhausted`], with the honest "only the user can end it" |
//!
//! ## The kill allowlist — the only processes this bridge may ever terminate
//!
//! [`TERMINABLE_IMAGES`], and only instances CREATED AFTER our own invoke.
//! Everything else is off limits, permanently:
//!
//! * **`3DEXPERIENCELauncher*` daemons are NEVER touched.** They are the
//! Platform's own background services. A successful launch on 2026-09-04
//! had no Backbone running at all, so they are not even required — killing
//! them buys nothing and breaks the user's Platform session. A real user
//! launch rebuilds them; this bridge does not get to decide that.
//! * **`sldworks.exe` is NEVER killed here.** Ending the user's CAD session is
//! `swx_kill`'s job, and that verb is gated behind the process-level write
//! opt-in, a `reason` and a `confirm` for exactly this reason.
//! * **No image-name kill anywhere.** Every terminate is by pid, re-checked
//! against the live census. See [`crate::launchos::LaunchOs::terminate`].
//!
//! ## Round 2 (0.4.1) — what the adversary found in the above
//!
//! The first version scoped the kill list BY CLOCK: every terminable image
//! younger than our invoke. Two consequences, both proved by test:
//!
//! * a chain the USER started while ours was in flight was swept up with ours
//! (pids 70003/70004) — and twenty seconds of a screen doing nothing is
//! exactly when somebody reaches for their own Start Menu;
//! * a one-second "clock slack" was applied to the PROCESS's age rather than
//! to ours, so a chain created a second BEFORE our invoke was terminable.
//!
//! The kill list is now the chain we RECORDED — pids that appeared after our
//! invoke and were absent from the baseline census taken immediately before it,
//! plus their descendants by parent pid — each re-verified as (pid, image,
//! creation time) on the open handle at the moment of the kill. There is no
//! backward slack anywhere, and `swx-bridge` reads its own clock through the
//! same call the creation times come from.
use crate::launchos::{epoch_secs, LaunchOs, ProcInfo};
use serde_json::{json, Value};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
// ── routes ──────────────────────────────────────────────────────────────────
/// How the shortcut is invoked, in the order this bridge tries them.
///
/// **ATTRIBUTABLE ROUTES FIRST.** The order used to be about which mechanism
/// was most likely to make the launcher work; it is now about which one can
/// tell us WHAT IT STARTED, because a route that cannot is a route whose chain
/// this bridge may never clean up:
///
/// | Route | Attribution | Show state |
/// |---|---|---|
/// | `shellExecute` | `SEE_MASK_NOCLOSEPROCESS` -> `hProcess` -> pid + creation time | explicit `SW_SHOWNORMAL` |
/// | `powershell` | `Start-Process -PassThru` prints the pid; the child is waited on | `Start-Process`'s own normal |
/// | `explorer` | **NONE** — the shell forwards the link and returns nothing | the shell's own |
/// | `cmdStart` | **NONE** — `start` returns as soon as it has handed off | detached, no window |
///
/// `explorer` and `cmdStart` are kept because they may be the routes that
/// actually WORK (that is still unmeasured), but a stall on either is reported
/// as `cleanup:"notAttributable"` and nothing is terminated.
pub const ROUTES: &[&str] = &["shellExecute", "powershell", "explorer", "cmdStart"];
/// Can this route say what it started? Routes that cannot are still tried and
/// still watched — they simply never earn the right to end a process.
pub fn route_is_attributable(route: &str) -> bool {
matches!(route, "shellExecute" | "powershell")
}
/// A route name we will actually invoke. Anything else is refused rather than
/// passed to a process spawner.
pub fn is_route(name: &str) -> bool {
ROUTES.contains(&name)
}
/// The plan for one attempt: the remembered route first (if it is still a real
/// route), then every other route in the standard order. No duplicates.
pub fn route_plan(preferred: Option<&str>) -> Vec<String> {
let mut plan: Vec<String> = Vec::new();
if let Some(p) = preferred.filter(|p| is_route(p)) {
plan.push(p.to_string());
}
for r in ROUTES {
if !plan.iter().any(|p| p == r) {
plan.push((*r).to_string());
}
}
plan
}
// ── the kill allowlist ──────────────────────────────────────────────────────
/// The ONLY images this bridge may ever terminate as part of a launch recovery.
pub const TERMINABLE_IMAGES: &[&str] = &["CATSTART.exe", "SWXDesktopLauncher.exe"];
/// Images that are off limits whatever else is true. `3DEXPERIENCELauncher` is
/// matched as a PREFIX so a renamed or versioned daemon cannot slip through a
/// list of exact names.
pub const NEVER_TERMINATE_PREFIXES: &[&str] = &["3DEXPERIENCELauncher"];
/// In the census as PARENT LINKS, and never as targets. The shells are how a
/// chain root is attributed; terminating one would take the user's desktop
/// (explorer.exe) or this bridge's own tooling with it.
pub const NEVER_TERMINATE: &[&str] = &[
"SLDWORKS.exe",
"sldworks.exe",
"swCefSubProc.exe",
"explorer.exe",
"cmd.exe",
"powershell.exe",
"pwsh.exe",
"swx-bridge.exe",
];
/// MAY THIS BRIDGE TERMINATE THIS PROCESS? The whole safety property, as one
/// pure function, so every branch of it is testable and none of it depends on
/// what a caller passed in.
///
/// Four conditions, ALL required:
/// 1. the image is on [`TERMINABLE_IMAGES`];
/// 2. the image is not on either never-terminate list;
/// 3. the creation time is KNOWN — an unreadable creation time is "not ours",
/// never "probably ours";
/// 4. it was created **at or after** our own invoke. NO BACKWARD SLACK: the
/// first version subtracted a second here "for clock skew", which made a
/// chain created a second BEFORE our invoke terminable — and a second is
/// exactly how long it takes somebody to click their own Start Menu while
/// an agent is telling them it is starting SOLIDWORKS. Skew is dealt with
/// where it belongs instead: `LaunchOs::now()` reads the SAME clock the
/// creation times come from, through the same conversion.
///
/// **This function is necessary and not sufficient.** It answers "could this
/// class of process ever be ours"; WHICH processes are ours is decided by
/// [`chain_candidates`] from a baseline census, and the kill itself re-verifies
/// identity on the open handle.
pub fn may_terminate(image: &str, created_at: Option<u64>, invoked_at: u64) -> bool {
if NEVER_TERMINATE.iter().any(|n| n.eq_ignore_ascii_case(image)) {
return false;
}
let lower = image.to_ascii_lowercase();
if NEVER_TERMINATE_PREFIXES.iter().any(|p| lower.starts_with(&p.to_ascii_lowercase())) {
return false;
}
if !TERMINABLE_IMAGES.iter().any(|t| t.eq_ignore_ascii_case(image)) {
return false;
}
match created_at {
None => false,
Some(at) => at >= invoked_at,
}
}
/// THE ROOT OF OUR CHAIN, as the invoke itself reported it.
///
/// `None` means the route could not say — and that is a first-class answer, not
/// a gap to be filled in by looking around the machine.
#[derive(Clone, Debug, PartialEq)]
pub struct Attribution {
pub pid: u32,
pub created_at: Option<u64>,
/// How we came to believe it: `shellExecuteHandle`, `startProcessPassThru`.
pub source: String,
}
impl Attribution {
pub fn to_json(&self) -> Value {
json!({"pid": self.pid, "createdAt": self.created_at, "source": self.source})
}
/// The identity this pins, for `ProcInfo::same_process`.
pub fn as_proc(&self, image: &str) -> ProcInfo {
ProcInfo { pid: self.pid, image: image.to_string(), created_at: self.created_at, ppid: None }
}
}
/// **NO HANDLE, NO KILL.**
///
/// The law this module is built on, stated once. A process may be terminated by
/// this bridge only if its membership of our launcher chain descends, through
/// parent links, from a root the INVOKE ITSELF handed back — a `hProcess` from
/// `ShellExecuteExW`, or a pid printed by `Start-Process -PassThru`. Nothing
/// found by searching the machine afterwards is ever adopted.
///
/// The rule this replaces was "the first CATSTART to appear after our invoke, in
/// census order, is ours". Three things made that a guess and not an inference:
///
/// * census order is roughly pid order and pids do not record who asked;
/// * parentage could not break the tie, because the census contained no
/// shells, so the root's parent link always dangled;
/// * on the `explorer` route our CATSTART and a Start-Menu CATSTART have
/// literally the SAME parent — the user's `explorer.exe`.
///
/// So on a run where our route started nothing at all (which this bridge now
/// detects and names), a launch the user started while waiting was the only
/// candidate — and was adopted, and terminated. That is the finding this rule
/// exists to make structurally impossible.
pub struct AttributionRule;
/// Grow the chain DOWNWARD from pids already proven ours.
///
/// Strictly descendants: a seed pid is never re-adopted as a member by its
/// number. Membership of the seeds themselves is re-confirmed elsewhere with
/// [`ProcInfo::same_process`] against the recorded identities — because CATSTART
/// exits about three seconds into a healthy launch and its number is then free
/// for the rest of a four-minute watch, so anything that takes it and happens to
/// be a launcher image would otherwise be re-adopted by number alone.
///
/// With NO seeds there is nothing to descend from and the answer is EMPTY. That
/// is the whole of the fix: this function can no longer invent a root.
pub fn chain_candidates(
census: &[ProcInfo],
baseline_pids: &[u32],
seed_pids: &[u32],
invoked_at: u64,
) -> Vec<ProcInfo> {
// The earliest creation time among the seeds: nothing descending from them
// can be older than the oldest of them.
let root_created = census
.iter()
.filter(|p| seed_pids.contains(&p.pid))
.filter_map(|p| p.created_at)
.min();
let mut ids: Vec<u32> = seed_pids.to_vec();
let mut ours: Vec<ProcInfo> = Vec::new();
let mut changed = true;
while changed {
changed = false;
for p in census {
if ids.contains(&p.pid) || baseline_pids.contains(&p.pid) {
continue;
}
if !may_terminate(&p.image, p.created_at, invoked_at) {
continue;
}
// DESCENT IS THE ONLY WAY IN. No root rule, no "first one wins".
// And a child cannot pre-date its parent: a pid whose number
// happens to point at one of ours, on a process that already
// existed when ours started, is not our child.
let after_parent = match (p.created_at, root_created) {
(Some(child), Some(root)) => child >= root,
_ => true,
};
if after_parent && p.ppid.map(|pp| ids.contains(&pp)).unwrap_or(false) {
ids.push(p.pid);
ours.push(p.clone());
changed = true;
}
}
}
ours
}
/// The whole chain: the attributed ROOT (re-confirmed as the same process), the
/// members we already confirmed, and everything descending from them.
///
/// Returns `None` when there is no attribution — the caller must then record
/// nothing and, on a stall, report that a chain may remain which it cannot
/// identify.
pub fn attributed_chain(
census: &[ProcInfo],
root: Option<&Attribution>,
recorded: &[ProcInfo],
baseline_pids: &[u32],
invoked_at: u64,
) -> Option<Vec<ProcInfo>> {
let root = root?;
// A root whose creation time we could not read is not an identity.
root.created_at?;
let mut chain: Vec<ProcInfo> = Vec::new();
// The root is a MEMBER only if the census can see it and it is terminable.
if let Some(live) = census
.iter()
.find(|c| c.pid == root.pid && c.created_at == root.created_at)
{
if may_terminate(&live.image, live.created_at, invoked_at) {
chain.push(live.clone());
}
}
// Members confirmed on an earlier pass, re-confirmed now as the SAME
// processes — never by number.
for r in recorded {
if chain.iter().any(|c| c.pid == r.pid) {
continue;
}
if let Some(live) = census.iter().find(|c| c.same_process(r)) {
chain.push(live.clone());
}
}
// THE ROOT PID SEEDS THE WALK UNCONDITIONALLY. Being terminable and being
// proof of lineage are different things, and the first version conflated
// them: `ShellExecuteExW` hands back whatever process the shell created for
// the `.lnk`, and if that is anything but the two watched launcher images —
// a shim, a wrapper, a future Platform build — the root was never seeded,
// the walk had nothing to descend from, and the real CATSTART it started
// (whose ppid is right there in the census, pointing at our handle-proven
// pid) was never attributed. The supervisor then read the empty result as
// "nothingToClean" and moved on with a chain of ours still running.
let mut seeds: Vec<u32> = chain.iter().map(|p| p.pid).collect();
// ...UNLESS THE MACHINE CONTRADICTS IT. A census entry with the root's pid
// and a DIFFERENT creation time means that number now belongs to something
// else, so its children are somebody else's children and the lineage is
// void. A root the census simply cannot SEE (an unwatched image, or one
// that has already exited) is not contradicted — that is the whole point.
let contradicted = census
.iter()
.any(|c| c.pid == root.pid && c.created_at.is_some() && c.created_at != root.created_at);
if !contradicted && !seeds.contains(&root.pid) {
seeds.push(root.pid);
}
chain.extend(chain_candidates(census, baseline_pids, &seeds, invoked_at));
Some(chain)
}
// ── stages ──────────────────────────────────────────────────────────────────
/// How far the launch has got. Reported verbatim to callers as `launch.stage`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Stage {
/// The route was invoked; nothing has been observed yet.
Invoked,
/// A NEW `SWXDesktopLauncher` log appeared — the chain is running.
LauncherStarted,
/// The launcher's second thread logged `Run Begin`: it is locating and
/// spawning `sldworks.exe`. This is the transition that never happened.
RunBegin,
/// A `SWXDesktopHelper` log appeared: sign-in / licence.
HelperSignIn,
/// `sldworks.exe` exists.
SldworksUp,
/// `sldworks.exe` exists and we are polling for a COM server.
Attaching,
/// COM-attached. Terminal, and the only success.
Attached,
/// This route stalled and its chain was cleaned up. Terminal for the route.
Stalled,
/// Every route was tried. Terminal for the attempt.
Exhausted,
}
impl Stage {
pub fn name(&self) -> &'static str {
match self {
Stage::Invoked => "invoked",
Stage::LauncherStarted => "launcherStarted",
Stage::RunBegin => "runBegin",
Stage::HelperSignIn => "helperSignIn",
Stage::SldworksUp => "sldworksUp",
Stage::Attaching => "attaching",
Stage::Attached => "attached",
Stage::Stalled => "stalled",
Stage::Exhausted => "exhausted",
}
}
/// What a caller should DO about this stage. Every one of these is written
/// for an AI reading the response and nothing else.
pub fn advice(&self) -> &'static str {
match self {
Stage::Invoked => "The launcher was invoked moments ago. Poll swx_status.",
Stage::LauncherStarted =>
"The launcher chain is running but has not reached 'Run Begin' yet. This is the \
stage that stalls; the bridge is watching for it and will clean up and try the \
next route on its own. Do NOT invoke the shortcut any other way.",
Stage::RunBegin =>
"The launcher is locating and spawning sldworks.exe. SOLIDWORKS appears within a \
few seconds; COM attach follows in 1-3 minutes.",
Stage::HelperSignIn =>
"SOLIDWORKS is signing in to the 3DEXPERIENCE tenant. This can stop at a sign-in \
window only the user can answer — if it sits here, ask them to look at their screen.",
Stage::SldworksUp | Stage::Attaching =>
"sldworks.exe is up; the COM server is not answering yet. A Connected cold start \
runs 1-3 minutes. Keep polling swx_status; do not launch again.",
Stage::Attached => "SOLIDWORKS is up and COM-attached. Carry on.",
Stage::Stalled =>
"That route stalled and its chain was cleaned up. The bridge is trying the next route.",
Stage::Exhausted =>
"Every launch route was tried and none reached a COM-attachable SOLIDWORKS. ASK \
THE USER to start SOLIDWORKS from the Start Menu or the 3DEXPERIENCE Platform; \
an interactive launch works when this one does not, and once it is up every \
other verb in this bridge behaves normally.",
}
}
}
/// THE 3DEXPERIENCE ID SIGN-IN WINDOW.
///
/// Live, 2026-09-11: a cold launch stopped at a NATIVE WebView2 window — class
/// `DSYSysLoginPanelWebView2Class`, title "Login | 3DEXPERIENCE ID | Dassault
/// Systemes", owned by `SWXDesktopLauncher.exe` in the bridge's OWN chain, and
/// with no controls visible to UIA at all. Nothing can fill it, including this
/// bridge; only the user can.
///
/// The launch machine did not know that window existed, so its
/// "sldworks.exe within 120s" deadline fired while Drew was reading the sign-in
/// prompt, and the machine KILLED ITS OWN CHAIN — including the window he was
/// about to type into — escalated to the next route, killed that too, and
/// finished `exhausted` with advice to ask the user. SOLIDWORKS then started
/// anyway, from a surviving window, and the bridge recorded the session as not
/// its own.
///
/// A SIGN-IN WAIT IS NOT A STALL. It is the launch working correctly and waiting
/// for a human, and a human is slower than any budget worth setting.
pub const SIGN_IN_CLASSES: &[&str] = &["DSYSysLoginPanelWebView2Class"];
/// Title fragments, as a fallback for a build whose class name differs. Both
/// must match — a window is a sign-in only if its title carries the platform's
/// own name AND says what it is.
pub fn is_sign_in_window(class_name: &str, title: &str) -> bool {
if SIGN_IN_CLASSES.iter().any(|c| c.eq_ignore_ascii_case(class_name)) {
return true;
}
let t = title.to_ascii_lowercase();
(t.contains("3dexperience id") || t.contains("dassault"))
&& (t.contains("login") || t.contains("sign in") || t.contains("sign-in"))
}
/// TELL THE USER, once, that the launch is waiting on them.
///
/// A paused deadline is invisible: without this the bridge would sit silently
/// while a window nobody mentioned waited on a desk somebody had walked away
/// from. Through ab, so it is the same native toast every other bridge uses; a
/// failure here never fails the launch (the wait is still correct), it just
/// means nobody was told.
fn announce_sign_in(title: &str) {
let r = crate::caller::ad_callback(
"notify_user",
json!({
"title": "SOLIDWORKS needs your 3DEXPERIENCE sign-in",
"message": format!(
"The launch is waiting at \"{title}\". Type your password in that window and tick \
\"Remember me\" — or, if your 3DEXPERIENCE login is saved in your local vault, \
ask the AI to sign you in (swx_sign_in). The bridge never stores a password."
),
}),
Duration::from_secs(10),
);
crate::ledger::record(
"solidworks_launch_sign_in_notified",
json!({"delivered": r.get("success").and_then(|v| v.as_bool()), "title": title}),
);
}
/// Is one of OUR chain's processes showing a sign-in window right now?
///
/// Attribution by OWNER PID, never by class name alone: a sign-in window
/// belonging to somebody else's launch is not ours to wait for. `None` means the
/// window census could not be taken for any of our pids — which is NOT "no
/// sign-in": an unreadable census must not resume a deadline that would kill the
/// very window it could not see.
pub fn sign_in_window(os: &dyn LaunchOs, ours: &[u32]) -> Option<Option<(i64, String)>> {
let mut any_read = false;
let census = os.processes().ok();
let image_of = |pid: u32| census.as_ref().and_then(|c| c.iter().find(|p| p.pid == pid).map(|p| p.image.clone())).unwrap_or_default();
for pid in ours {
match os.windows_of_pid(*pid) {
Ok(ws) => {
any_read = true;
// The fast path (class / title), then CONTENT (0.9.23).
if let Some(w) = ws.iter().find(|w| is_sign_in_window(&w.class_name, &w.title)) {
return Some(Some((w.hwnd, w.title.clone())));
}
let image = image_of(*pid);
if let Some(w) = ws.iter().find(|w| crate::signin::window_is_login(&image, w).is_some()) {
return Some(Some((w.hwnd, w.title.clone())));
}
}
Err(_) => {}
}
}
// THE PLATFORM UPDATER'S LOGIN (0.9.23, MEASURED 2026-10-12): hosted by
// SWXDesktopUpdate.exe, which the chain census does not link. The platform
// session gates every launch, so while that login is up this launch is
// waiting on the user too — a pause is always the safe answer.
if !ours.is_empty() {
if let Some(found) = crate::signin::desktop_update_login(os) {
return Some(Some(found));
}
}
if any_read {
Some(None)
} else {
None
}
}
/// IS OUR LAUNCH SHOWING A SIGN-IN WINDOW RIGHT NOW? From a LIVE census of the
/// recorded chain's pids — so it is right even when the monitor thread that
/// first saw the window died with an older bridge process (0.9.13; measured
/// 2026-10-02: after a respawn mid-launch, swx_status said `helperSignIn` with
/// `waitingOn` null). `None` = the windows could not be read.
pub fn sign_in_now(os: &dyn LaunchOs) -> Option<Option<(i64, String)>> {
let ours = chain_pids_now();
if ours.is_empty() {
return Some(None);
}
sign_in_window(os, &ours)
}
pub fn note_sign_in_inventory(inv: Value) {
update(move |a| a.sign_in_inventory = Some(inv));
}
pub fn note_sign_in_wake(wake: Value) {
update(move |a| a.sign_in_wake = Some(wake));
}
pub fn note_sign_in_rejected() {
update(|a| a.sign_in_rejected = true);
}
/// `swx_start {signIn:"auto"}` — recorded on the attempt in flight; if the
/// sign-in window is ALREADY up and no unattended attempt has been made, it is
/// made now (once).
pub fn arm_auto_sign_in(os: &dyn LaunchOs) {
update(|a| a.sign_in_auto = true);
if let Some(Some((hwnd, _))) = sign_in_now(os) {
on_sign_in_window(hwnd);
}
}
/// INVENTORY + (when armed) THE UNATTENDED SIGN-IN, off the monitor thread so
/// the launch's own polling never waits on ab.
fn on_sign_in_window(hwnd: i64) {
std::thread::spawn(move || {
if let Ok(r) = crate::signin::read_page(hwnd) {
note_sign_in_inventory(crate::signin::inventory_json(&r.elements));
note_sign_in_wake(r.wake.clone());
}
let auto = attempt().map(|a| a.sign_in_auto && !a.sign_in_auto_tried && !a.sign_in_rejected).unwrap_or(false);
if auto {
update(|a| a.sign_in_auto_tried = true);
crate::signin::sign_in(
"swx_start {signIn:\"auto\"}: the launch reached the 3DEXPERIENCE sign-in",
crate::signin::Opts::default(),
crate::caller::self_caller(),
);
}
});
}
// ── tunables (test knobs; the defaults are the measured machine) ────────────
fn secs_env(name: &str, default: u64) -> u64 {
std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
/// `Run Begin` within this many seconds of the invoke, or the route has stalled.
/// Measured: a healthy launch logs it ~2s after `SecurityContext:`.
pub fn run_begin_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_RUN_BEGIN_SECS", 20))
}
/// `sldworks.exe` within this long of the invoke. Measured: ~3s after Run Begin.
pub fn sldworks_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_SLDWORKS_SECS", 120))
}
/// COM attach within this long of the invoke. Measured: 1-3 minutes.
pub fn attach_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_ATTACH_SECS", 240))
}
/// How long an unfinished attempt keeps refusing to launch again.
pub fn inflight_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_INFLIGHT_SECS", 240))
}
/// How long after a FAILED attempt before another full one may start.
///
/// A retry that immediately sweeps all four routes again is how four launcher
/// chains ended up on somebody's desktop. After a failure the bridge waits, and
/// then tries ONE route rather than the whole plan.
pub fn backoff_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_BACKOFF_SECS", 60))
}
/// THE 3DEXPERIENCE HELPER SIGN-IN may take this long before the launch gives
/// up on it (0.11.2) — and giving up STOPS, it never re-invokes. MEASURED
/// 2026-10-15: helperSignIn took 2 min 13 s on adom_drew2, the old clock
/// called that a stall, killed the chain and invoked the shortcut again — and
/// the re-invoke reached the already-starting SOLIDWORKS as an "open" request:
/// a File ▸ Open dialog that blocked every quit after it.
pub fn helper_budget() -> Duration {
Duration::from_secs(secs_env("SWX_LAUNCH_HELPER_SECS", 600))
}
/// Before re-invoking after a stall, watch this long for a SOLIDWORKS the
/// stalled route started anyway (measured: it came up 6 s after the cleanup).
pub fn retry_settle() -> Duration {
Duration::from_millis(secs_env("SWX_LAUNCH_RETRY_SETTLE_MS", 15_000))
}
/// How long after a route a dialog that appears is still attributed to it
/// (and, when it is a file dialog with an empty name, cancelled by it).
pub fn dialog_grace() -> Duration {
Duration::from_millis(secs_env("SWX_LAUNCH_DIALOG_GRACE_MS", 90_000))
}
/// A START RIGHT AFTER A QUIT (0.11.3). MEASURED 2026-10-15: a quit at
/// 22:40:01, a launch at 22:40:02 — and the new SOLIDWORKS died 11 s after
/// attach. A start within `SWX_START_AFTER_QUIT_SECS` (60) of a quit this bridge
/// made first waits, up to `SWX_START_AFTER_QUIT_WAIT_SECS` (30), for that
/// session's process tree to be gone: its sldworks.exe and the per-session
/// children (`quit::SESSION_CHILDREN`) — never the platform daemons, which
/// survive a quit by design.
pub fn previous_session_remaining(census: &[ProcInfo], prev_pid: u32, prev_created: Option<u64>, quit_at_secs: u64) -> Vec<ProcInfo> {
census
.iter()
.filter(|p| {
(p.is("SLDWORKS.exe") && p.pid == prev_pid && (prev_created.is_none() || p.created_at.is_none() || p.created_at == prev_created))
|| (crate::quit::SESSION_CHILDREN.iter().any(|c| p.is(c))
&& (p.ppid == Some(prev_pid) || p.created_at.map(|c| c <= quit_at_secs).unwrap_or(true)))
})
.cloned()
.collect()
}
/// Wait (bounded) for the last quit's process tree to be gone, ledgered.
/// `None` when no quit was recent.
pub fn wait_for_previous_exit(os: &dyn LaunchOs) -> Option<Value> {
let q: Value = std::fs::read_to_string(crate::scratch::root().join("last-quit.json")).ok().and_then(|t| serde_json::from_str(&t).ok())?;
let at_ms = q.get("atMs").and_then(|v| v.as_u64())?;
let pid = q.get("pid").and_then(|v| v.as_u64())? as u32;
let created = q.get("createdAt").and_then(|v| v.as_u64());
let now_ms = crate::launchos::epoch_ms() as u64;
if now_ms.saturating_sub(at_ms) > secs_env("SWX_START_AFTER_QUIT_SECS", 60) * 1000 {
return None;
}
let started = std::time::Instant::now();
let deadline = started + Duration::from_secs(secs_env("SWX_START_AFTER_QUIT_WAIT_SECS", 30));
let mut remaining: Vec<ProcInfo>;
let mut readable = true;
loop {
match os.processes() {
Ok(c) => remaining = previous_session_remaining(&c, pid, created, at_ms / 1000),
Err(_) => {
readable = false;
remaining = Vec::new();
}
}
if remaining.is_empty() || std::time::Instant::now() >= deadline || !readable {
break;
}
std::thread::sleep(Duration::from_millis(250));
}
let gone = readable && remaining.is_empty();
let v = json!({
"previous": {"pid": pid, "createdAt": created},
"quitSecondsAgo": now_ms.saturating_sub(at_ms) / 1000,
"waitedMs": started.elapsed().as_millis() as u64,
"gone": gone,
"censusReadable": readable,
"remaining": remaining.iter().map(|p| json!({"pid": p.pid, "image": p.image})).collect::<Vec<_>>(),
});
crate::ledger::record("launch_waited_for_previous_exit", v.clone());
Some(v)
}
/// A SOLIDWORKS that this launch did not find running before it began: any
/// sldworks.exe outside the FIRST route's baseline. While one exists, no route
/// is invoked again — a second invoke reaches it as an "open" request.
pub fn new_solidworks(census: &[ProcInfo], first_baseline: &[u32]) -> Option<ProcInfo> {
census.iter().find(|p| p.is("SLDWORKS.exe") && !first_baseline.contains(&p.pid)).cloned()
}
fn poll_interval() -> Duration {
Duration::from_millis(secs_env("SWX_LAUNCH_POLL_MS", 1000).max(50))
}
fn attach_poll_interval() -> Duration {
Duration::from_millis(secs_env("SWX_LAUNCH_ATTACH_POLL_MS", 3000).max(50))
}
// ── the crash reporter ──────────────────────────────────────────────────────
/// The ONE control this bridge may click during a launch. Identification is by
/// the BUTTON'S ACCESSIBLE NAME, not by a window title.
pub const CRASH_RESTART_BUTTON: &str = "Restart SOLIDWORKS";
/// Crash-reporter recovery is OFF unless this is `on`.
///
/// SHIPPED DISABLED. The click is decided from ab's `desktop_find_control`
/// reply, and **no reply from that verb has ever been seen** — the shape it
/// returns on a real desktop is a guess, and a guess about a reply shape is a
/// guess about whether to click something on somebody's screen. The live gate
/// captures the real reply; until it has, the bridge reports the crash reporter
/// and does nothing about it.
pub const ENV_CRASH_RECOVERY: &str = "SWX_CRASH_RECOVERY";
pub fn crash_recovery_enabled() -> bool {
std::env::var(ENV_CRASH_RECOVERY).as_deref() == Ok("on")
}
/// Phrases that make a window a crash reporter. A crash reporter SAYS a crash
/// happened; it does not merely mention SOLIDWORKS.
const CRASH_PHRASES: &[&str] =
&["stopped working", "stopped responding", "not responding", "error report", "crash report",
"has crashed", "document recovery", "unexpectedly"];
/// ...and phrases that prove it is something else. A browser tab about a crash,
/// a mail thread about one, and the Installation Manager asking for a restart
/// all contain the words a naive matcher looks for. The bridge sends a UIA probe
/// at every candidate, so candidacy is not free.
const NOT_A_CRASH_REPORTER: &[&str] = &[
"chrome", "firefox", "edge", "opera", "safari", "outlook", "teams", "slack", "gmail",
"installation manager", "install", "update", "upgrade", "setup", "re:", "fw:", "how to",
"- word", "- excel", "notepad", "visual studio", "explorer",
];
/// Is this window worth a read-only control probe? Pure, so the rule is visible
/// and testable rather than buried in a callback.
///
/// NOTE the asymmetry: a false NO costs one crash recovery the user can do
/// themselves in two clicks; a false YES sends a UIA probe at an arbitrary
/// window on somebody's desktop and, if the reply is read loosely, clicks it.
pub fn is_crash_reporter_candidate(title: &str) -> bool {
let t = title.to_lowercase();
if NOT_A_CRASH_REPORTER.iter().any(|bad| t.contains(bad)) {
return false;
}
if let Ok(exact) = std::env::var("SWX_CRASH_REPORTER_TITLE") {
if !exact.is_empty() && t.contains(&exact.to_lowercase()) {
return true;
}
}
t.contains("solidworks") && CRASH_PHRASES.iter().any(|w| t.contains(w))
}
/// Did ab's control lookup actually FIND the button?
///
/// Reads NAMED FIELDS ONLY. The first version stringified the whole reply and
/// searched it for the button name — and the bridge sends that name in the
/// request, so any reply that echoed its query passed the gate with zero
/// controls found. A reply is a find when it says so structurally: an explicit
/// `found:true`, or a control/element entry whose own `name`/`automationId`/
/// `text` names the button. An empty list is a NO, and an unrecognised shape is
/// a NO — the bridge does not click on a reply it does not understand.
pub fn mentions_button(resp: &Value) -> bool {
if resp.get("success").and_then(|v| v.as_bool()) != Some(true) {
return false;
}
if resp.get("found").and_then(|v| v.as_bool()) == Some(false) {
return false;
}
let names_it = |v: &Value| -> bool {
["name", "automationId", "automationID", "text", "label"].iter().any(|k| {
v.get(*k)
.and_then(|x| x.as_str())
.map(|s| s.to_lowercase().contains(&CRASH_RESTART_BUTTON.to_lowercase()))
.unwrap_or(false)
})
};
// A list of controls: at least one entry must name the button itself.
for key in ["controls", "elements", "matches", "results", "children"] {
if let Some(arr) = resp.get(key).and_then(|v| v.as_array()) {
return arr.iter().any(names_it);
}
}
// A single control object, inline or under `control`.
if let Some(c) = resp.get("control").filter(|c| c.is_object()) {
return names_it(c);
}
if names_it(resp) {
// A flat reply naming the control — accepted only alongside a positive
// `found`/`invokable`, so the query echo (which has neither) cannot.
return resp.get("found").and_then(|v| v.as_bool()) == Some(true)
|| resp.get("invokable").and_then(|v| v.as_bool()) == Some(true);
}
false
}
// ── the persisted attempt ───────────────────────────────────────────────────
/// One route's turn inside an attempt, as it is recorded.
#[derive(Clone, Debug)]
pub struct RouteTry {
pub route: String,
pub invoked_at: u64,
pub stage: Stage,
pub stalled: bool,
pub killed: Vec<ProcInfo>,
/// The cleanup could not be PERFORMED — the census failed, or a terminate
/// was refused. `killed:[]` alone is indistinguishable from "there was
/// nothing to kill", which is what a clean recovery looks like.
pub cleanup_failed: bool,
pub cleanup_error: Option<String>,
/// Chain processes known to be still running after the cleanup.
pub chain_left_running: Vec<ProcInfo>,
/// Did the INVOKE tell us what it started? Without this, nothing may be
/// terminated, however obvious the chain on the machine looks.
pub attributed: bool,
/// `cleaned` | `notAttributable` | `notConfirmed` | `failed` | `nothingToClean`.
pub cleanup: &'static str,
/// A launcher chain from this route may still be running and this bridge
/// cannot name it. Distinct from `chain_left_running`, which lists
/// processes it CAN name and could not end.
pub chain_may_remain: bool,
// ── THIS ROUTE'S OWN EVIDENCE ────────────────────────────────────────
// These were read off the ATTEMPT by the gate's per-route table, which
// printed the same value on every row: the last route's log attributed to
// all of them. A per-route table whose rows are all the same number is not
// a measurement.
pub launcher_log: Option<String>,
pub run_begin_seen: bool,
pub unattributed_log: bool,
pub root_pid: Option<u32>,
pub root_confirmed: bool,
pub note: Option<String>,
}
impl RouteTry {
fn to_json(&self) -> Value {
json!({
"route": self.route,
"invokedAt": self.invoked_at,
"stage": self.stage.name(),
"stalled": self.stalled,
"killed": self.killed.iter().map(|p| p.to_json()).collect::<Vec<_>>(),
"cleanupFailed": self.cleanup_failed,
"cleanupError": self.cleanup_error,
"chainLeftRunning": self.chain_left_running.iter().map(|p| p.to_json()).collect::<Vec<_>>(),
"attributed": self.attributed,
"cleanup": self.cleanup,
"chainMayRemain": self.chain_may_remain,
"launcherLog": self.launcher_log,
"runBeginSeen": self.run_begin_seen,
"unattributedLog": self.unattributed_log,
"rootPid": self.root_pid,
"rootConfirmed": self.root_confirmed,
"note": self.note,
})
}
}
/// The launch attempt this process is running (or last ran).
#[derive(Clone, Debug)]
pub struct Attempt {
pub id: u64,
pub started_at: u64,
pub shortcut: String,
pub reason: String,
pub caller: String,
pub plan: Vec<String>,
pub route: String,
pub invoked_at: u64,
pub stage: Stage,
pub stage_at: u64,
/// The chain WE started, by identity. The kill path uses this and nothing
/// else.
pub chain: Vec<ProcInfo>,
/// Terminable processes that were ALREADY running when we invoked. Never
/// ours, whatever their age says later.
pub baseline_pids: Vec<u32>,
/// Could the baseline census be taken at all? `false` means this bridge
/// cannot see the machine it is starting processes on.
pub baseline_known: bool,
/// What the INVOKE said it started. The only authority to terminate.
pub attribution: Option<Attribution>,
pub launcher_log: Option<String>,
/// Did `Run Begin` appear in a launcher log we could prove was OURS? The
/// only evidence that the route did anything — and the only thing that may
/// make a route "preferred".
pub run_begin_seen: bool,
/// Was the user asked to sign in to 3DEXPERIENCE during this attempt? It
/// changes what every timing in this record MEANS — a launch that took eight
/// minutes because somebody was typing a password is not a slow launch.
pub sign_in_seen: bool,
/// THE SIGN-IN WINDOW ON SCREEN NOW, by title (0.9.12) — `None` when none is
/// up. What `waitingOn:"userSignIn"` names.
pub sign_in_window: Option<String>,
/// THE LOGIN WINDOW'S ACCESSIBLE INVENTORY, captured every time a launch
/// reaches the sign-in (0.9.13): control types, names, automation ids,
/// IsPassword, toggle states — never a value.
pub sign_in_inventory: Option<Value>,
/// How that inventory was read: ab's node count on each read (cold first)
/// and how many wakes it took (0.9.15 — the tree is lazy).
pub sign_in_wake: Option<Value>,
/// The saved password was rejected in this launch: never retried.
pub sign_in_rejected: bool,
/// `swx_start {signIn:"auto"}`: the bridge may sign in by itself, ONCE.
pub sign_in_auto: bool,
pub sign_in_auto_tried: bool,
/// The launch record ended (abandoned, timed out) before SOLIDWORKS came up,
/// and SOLIDWORKS was later found attached — the clause re-attribution gave.
pub superseded_by_attach: Option<String>,
/// LAUNCHER PROCESSES THAT APPEARED AFTER OUR INVOKE AND CANNOT BE PROVED
/// EITHER WAY. An unattributable route (`explorer`, `cmdStart`) cannot say
/// what it started, so a launcher that shows up while one is running is
/// UNATTRIBUTED — possibly ours, possibly not. It is recorded here so the
/// record is honest about what was on the machine, and it is excluded from
/// BOTH the foreign set and the kill path: "possibly ours" is a reason not to
/// accuse and not to terminate, which happen to be the same answer.
pub possibly_ours: Vec<u32>,
/// WHEN EACH CHAIN PROCESS WAS LAST SEEN ALIVE, as `(pid, created_at, epoch
/// seconds)`.
///
/// A dead parent may be accepted as lineage — the launcher chain always exits
/// once `sldworks.exe` is up, so requiring it alive would make the walk
/// useless — but "a pid we once recorded" is not enough on its own: once that
/// process dies the NUMBER is free, and a user's launcher that reuses it
/// would have its SOLIDWORKS record our dead pid as its parent, with nothing
/// alive left to contradict it. So the window in which a child may claim that
/// parent is bounded by when we last watched the parent run.
pub chain_last_seen: Vec<(u32, u64, u64)>,
/// THE SOLIDWORKS THIS ATTEMPT'S ATTRIBUTION IS ABOUT: `(pid, creation
/// time)`, set when a session is re-claimed after a sign-in. `None` until
/// then. Read by the verbs that END a session, so that an attribution record
/// can never authorise acting on a DIFFERENT process than the one it was
/// made about.
pub attributed_sldworks: Option<(u32, u64)>,
/// HOW `attributed_sldworks` was established (0.10.7): `ancestry` (a parent
/// walk back to our own chain), `newProcessInLaunchWindow` (the one
/// sldworks.exe that was not in the baseline and was created between the
/// attempt's start and the attach), or `reattributedAfterSignIn`.
pub attributed_how: Option<String>,
/// Did we ever see the attributed root, or anything descending from it?
/// An attributed route whose chain was NEVER confirmed cannot be cleaned
/// up, and that is a failure to account for what we started — not a clean
/// "there was nothing to kill".
pub root_confirmed: bool,
/// Which route was in flight when COM attached. An ORDERING HINT ONLY —
/// `attached` is proof that SOLIDWORKS is up, never proof that this route
/// put it there. Reported with `"proof": false` and never promoted.
pub last_route_before_attach: Option<String>,
/// A launcher chain that is not ours was seen alive during this route's
/// watch. It is what stops "the only new log must be ours" being true.
pub foreign_chain_seen: bool,
/// A NEW launcher log appeared while this route was watched, and it could
/// not be attributed to us. Evidence that something started; no evidence
/// whose it is.
pub unattributed_log: bool,
pub tries: Vec<RouteTry>,
pub crash_recovery_clicked: bool,
/// The bridge process supervising this attempt. A LATER bridge reading this
/// file needs to tell "somebody is watching this" from "this was left
/// behind by a process that is gone".
pub supervisor_pid: Option<u32>,
pub finished: bool,
pub outcome: Option<String>,
pub error: Option<String>,
pub os_kind: &'static str,
}
impl Attempt {
pub fn age_secs(&self) -> u64 {
epoch_secs().saturating_sub(self.started_at)
}
/// Any chain process this attempt could not end.
pub fn left_running(&self) -> Vec<Value> {
self.tries
.iter()
.flat_map(|t| t.chain_left_running.iter())
.map(|p| p.to_json())
.collect()
}
pub fn cleanup_failed(&self) -> bool {
self.tries.iter().any(|t| t.cleanup_failed)
}
pub fn to_json(&self) -> Value {
json!({
"id": self.id,
"startedAt": self.started_at,
"ageSeconds": self.age_secs(),
"shortcut": self.shortcut,
"reason": self.reason,
"caller": self.caller,
"routePlan": self.plan,
"route": self.route,
"invokedAt": self.invoked_at,
"stage": self.stage.name(),
"stageAgeSeconds": epoch_secs().saturating_sub(self.stage_at),
"chainPids": self.chain.iter().map(|p| p.to_json()).collect::<Vec<_>>(),
"baselinePids": self.baseline_pids,
"baselineKnown": self.baseline_known,
"attribution": self.attribution.as_ref().map(|a| a.to_json()),
"attributed": self.attribution.is_some(),
"launcherLog": self.launcher_log,
"runBeginSeen": self.run_begin_seen,
"signInSeen": self.sign_in_seen,
"signInWindow": self.sign_in_window,
"signInInventory": self.sign_in_inventory,
"signInWake": self.sign_in_wake,
"signInRejected": self.sign_in_rejected,
"signInMode": if self.sign_in_auto { "auto" } else { "wait" },
"signInAutoTried": self.sign_in_auto_tried,
"supersededByAttach": self.superseded_by_attach,
// Launcher processes that appeared while an UNATTRIBUTABLE route was
// running: not proved ours, not proved anybody else's, and never
// terminated on either reading.
"possiblyOurs": self.possibly_ours,
"attributedSolidworks": self.attributed_sldworks
.map(|(p, c)| json!({"pid": p, "createdAt": c, "how": self.attributed_how})),
"chainLastSeen": self.chain_last_seen
.iter()
.map(|(p, c, t)| json!([p, c, t]))
.collect::<Vec<_>>(),
"rootConfirmed": self.root_confirmed,
"unattributedLog": self.unattributed_log,
"foreignChainSeen": self.foreign_chain_seen,
"lastRouteBeforeAttach": self.last_route_before_attach.as_ref().map(|r| json!({
"route": r,
"proof": false,
"note": "the route in flight when COM attached. NOT evidence that it did the \
launching — the user may have started SOLIDWORKS themselves — so it is \
never remembered as preferredRoute. An ordering hint for a human.",
})),
"routesTried": self.tries.iter().map(|t| t.to_json()).collect::<Vec<_>>(),
"cleanupFailed": self.cleanup_failed(),
"chainLeftRunning": self.left_running(),
"crashRecovery": if !crash_recovery_enabled() {
"off"
} else if self.crash_recovery_clicked {
"clicked"
} else {
"armed"
},
"supervisorPid": self.supervisor_pid,
"finished": self.finished,
"outcome": self.outcome,
"error": self.error,
"observedBy": self.os_kind,
})
}
}
/// Everything the launch machine persists, next to `com-host-created.json` in
/// the bridge's own scratch root.
#[derive(Clone, Debug, Default)]
pub struct LaunchStore {
pub preferred_route: Option<String>,
pub attempt: Option<Attempt>,
pub history: Vec<Value>,
/// How many attempts in a row have failed. Drives the backoff.
pub consecutive_failures: u32,
/// When the last attempt finished without attaching.
pub last_failure_at: Option<u64>,
}
const HISTORY_MAX: usize = 20;
pub fn state_path() -> std::path::PathBuf {
crate::scratch::root().join("launch-state.json")
}
fn stage_from_name(s: &str) -> Stage {
match s {
"launcherStarted" => Stage::LauncherStarted,
"runBegin" => Stage::RunBegin,
"helperSignIn" => Stage::HelperSignIn,
"sldworksUp" => Stage::SldworksUp,
"attaching" => Stage::Attaching,
"attached" => Stage::Attached,
"stalled" => Stage::Stalled,
"exhausted" => Stage::Exhausted,
_ => Stage::Invoked,
}
}
fn procs_from_json(v: Option<&Value>) -> Vec<ProcInfo> {
v.and_then(|a| a.as_array())
.map(|rows| {
rows.iter()
.filter_map(|r| {
Some(ProcInfo {
pid: r.get("pid")?.as_u64()? as u32,
image: r.get("image")?.as_str()?.to_string(),
created_at: r.get("createdAt").and_then(|c| c.as_u64()),
ppid: r.get("ppid").and_then(|c| c.as_u64()).map(|v| v as u32),
})
})
.collect()
})
.unwrap_or_default()
}
/// Read the store from disk. A missing file is an EMPTY store; an UNPARSEABLE
/// one is an empty store AND a loud line, because those are different facts.
pub fn load() -> LaunchStore {
let path = state_path();
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return LaunchStore::default(),
};
let v: Value = match serde_json::from_str(&text) {
Ok(v) => v,
Err(e) => {
eprintln!(
"[swx] launch-state.json could not be parsed ({e}); starting with no launch \
history. The file is kept as launch-state.json.bad."
);
let _ = std::fs::rename(&path, path.with_extension("json.bad"));
return LaunchStore::default();
}
};
let attempt = v.get("attempt").filter(|a| a.is_object()).map(|a| Attempt {
id: a.get("id").and_then(|x| x.as_u64()).unwrap_or(0),
started_at: a.get("startedAt").and_then(|x| x.as_u64()).unwrap_or(0),
shortcut: a.get("shortcut").and_then(|x| x.as_str()).unwrap_or("").to_string(),
reason: a.get("reason").and_then(|x| x.as_str()).unwrap_or("").to_string(),
caller: a.get("caller").and_then(|x| x.as_str()).unwrap_or("").to_string(),
plan: a
.get("routePlan")
.and_then(|x| x.as_array())
.map(|r| r.iter().filter_map(|s| s.as_str().map(String::from)).collect())
.unwrap_or_default(),
route: a.get("route").and_then(|x| x.as_str()).unwrap_or("").to_string(),
invoked_at: a.get("invokedAt").and_then(|x| x.as_u64()).unwrap_or(0),
stage: stage_from_name(a.get("stage").and_then(|x| x.as_str()).unwrap_or("")),
stage_at: a.get("stageAt").and_then(|x| x.as_u64()).unwrap_or(0),
chain: procs_from_json(a.get("chainPids")),
baseline_pids: a
.get("baselinePids")
.and_then(|x| x.as_array())
.map(|r| r.iter().filter_map(|p| p.as_u64().map(|v| v as u32)).collect())
.unwrap_or_default(),
baseline_known: a.get("baselineKnown").and_then(|x| x.as_bool()).unwrap_or(false),
attribution: a.get("attribution").filter(|x| x.is_object()).and_then(|x| {
Some(Attribution {
pid: x.get("pid")?.as_u64()? as u32,
created_at: x.get("createdAt").and_then(|c| c.as_u64()),
source: x.get("source").and_then(|c| c.as_str()).unwrap_or("restored").to_string(),
})
}),
launcher_log: a.get("launcherLog").and_then(|x| x.as_str()).map(String::from),
run_begin_seen: a.get("runBeginSeen").and_then(|x| x.as_bool()).unwrap_or(false),
sign_in_seen: a.get("signInSeen").and_then(|x| x.as_bool()).unwrap_or(false),
sign_in_window: a.get("signInWindow").and_then(|x| x.as_str()).map(String::from),
sign_in_inventory: a.get("signInInventory").filter(|x| !x.is_null()).cloned(),
sign_in_wake: a.get("signInWake").filter(|x| !x.is_null()).cloned(),
sign_in_rejected: a.get("signInRejected").and_then(|x| x.as_bool()).unwrap_or(false),
sign_in_auto: a.get("signInMode").and_then(|x| x.as_str()) == Some("auto"),
sign_in_auto_tried: a.get("signInAutoTried").and_then(|x| x.as_bool()).unwrap_or(false),
superseded_by_attach: a.get("supersededByAttach").and_then(|x| x.as_str()).map(String::from),
possibly_ours: a
.get("possiblyOurs")
.and_then(|x| x.as_array())
.map(|v| v.iter().filter_map(|p| p.as_u64().map(|n| n as u32)).collect())
.unwrap_or_default(),
attributed_sldworks: a.get("attributedSolidworks").and_then(|v| {
Some((
v.get("pid")?.as_u64()? as u32,
v.get("createdAt")?.as_u64()?,
))
}),
attributed_how: a.pointer("/attributedSolidworks/how").and_then(|v| v.as_str()).map(String::from),
chain_last_seen: a
.get("chainLastSeen")
.and_then(|x| x.as_array())
.map(|v| {
v.iter()
.filter_map(|r| {
let a = r.as_array()?;
Some((
a.first()?.as_u64()? as u32,
a.get(1)?.as_u64()?,
a.get(2)?.as_u64()?,
))
})
.collect()
})
.unwrap_or_default(),
root_confirmed: a.get("rootConfirmed").and_then(|x| x.as_bool()).unwrap_or(false),
unattributed_log: a.get("unattributedLog").and_then(|x| x.as_bool()).unwrap_or(false),
foreign_chain_seen: a.get("foreignChainSeen").and_then(|x| x.as_bool()).unwrap_or(false),
last_route_before_attach: a
.get("lastRouteBeforeAttach")
.and_then(|x| x.get("route"))
.and_then(|x| x.as_str())
.map(String::from),
tries: Vec::new(),
crash_recovery_clicked: a
.get("crashRecoveryClicked")
.and_then(|x| x.as_bool())
.unwrap_or(false),
// ABSENT means "written by a bridge that did not record one", which is
// treated exactly like a pid that is gone: not us, not alive.
supervisor_pid: a.get("supervisorPid").and_then(|x| x.as_u64()).map(|v| v as u32),
finished: a.get("finished").and_then(|x| x.as_bool()).unwrap_or(true),
outcome: a.get("outcome").and_then(|x| x.as_str()).map(String::from),
error: a.get("error").and_then(|x| x.as_str()).map(String::from),
os_kind: "restored",
});
LaunchStore {
preferred_route: v.get("preferredRoute").and_then(|x| x.as_str()).map(String::from),
attempt,
history: v.get("history").and_then(|x| x.as_array()).cloned().unwrap_or_default(),
consecutive_failures: v
.get("consecutiveFailures")
.and_then(|x| x.as_u64())
.unwrap_or(0) as u32,
last_failure_at: v.get("lastFailureAt").and_then(|x| x.as_u64()),
}
}
/// Persist the store ATOMICALLY: write a temp file, then rename over the real
/// one. A plain `fs::write` truncates first, so a kill (or a full disk) in the
/// middle of one leaves a half-file — and the launch machine would then read
/// "no attempt in flight" from a bridge that has one.
pub fn save(store: &LaunchStore) {
let mut history = store.history.clone();
if history.len() > HISTORY_MAX {
let drop = history.len() - HISTORY_MAX;
history.drain(0..drop);
}
let mut attempt = store.attempt.as_ref().map(|a| a.to_json());
if let (Some(obj), Some(a)) = (attempt.as_mut().and_then(|x| x.as_object_mut()), &store.attempt) {
obj.insert("stageAt".into(), json!(a.stage_at));
obj.insert("crashRecoveryClicked".into(), json!(a.crash_recovery_clicked));
}
let body = json!({
"preferredRoute": store.preferred_route,
"attempt": attempt,
"history": history,
"consecutiveFailures": store.consecutive_failures,
"lastFailureAt": store.last_failure_at,
});
if let Err(e) = write_atomic(&state_path(), &body.to_string()) {
// Loud, once per failure. An in-flight guard that silently stopped
// persisting would let the next swx_start start a second launch.
eprintln!("[swx] LAUNCH STATE NOT PERSISTED: {e}");
}
}
/// Temp file + rename, with the previous version kept as `.bak`.
///
/// RETRIES THE RENAME. On Windows an antivirus scanner or the search indexer
/// holds a brief exclusive handle on a file it has just seen created, and the
/// rename fails with ACCESS_DENIED for a few milliseconds — on a file this
/// bridge rewrites hundreds of times during one assembly build. A single
/// attempt therefore drops writes at random, and the one it drops might be the
/// one that recorded a document the bridge owns.
///
/// Returns whether the write landed. A failure is REPORTED (the callers log it)
/// rather than swallowed: "the bridge could not write its own notes" is a fact
/// somebody needs, and it is the beginning of every ownership bug in this file.
pub fn write_atomic(path: &std::path::Path, body: &str) -> Result<(), String> {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
// UNIQUE PER WRITE. The temp name used to be the pid alone, which is one
// name shared by every HTTP worker thread in this process — and the
// ownership registry is rewritten from several of them. Two concurrent
// writes then raced on one temp file: both wrote it, one renamed it away,
// and the other's rename failed with "no such file" (or, worse, renamed a
// half-written file into place).
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let tmp = path.with_extension(format!(
"tmp{}-{}",
std::process::id(),
SEQ.fetch_add(1, Ordering::SeqCst)
));
if let Err(e) = std::fs::write(&tmp, body) {
let _ = std::fs::remove_file(&tmp);
return Err(format!("could not write {}: {e}", tmp.display()));
}
// THE BACKUP IS STAGED TOO. `copy` straight onto `.bak` is not atomic
// either, so two concurrent writers could leave a torn backup — and the
// backup is what `owned::restore` falls back to when the live file cannot
// be parsed. Copy to a unique name, then rename it into place.
if path.exists() {
let bak_tmp = path.with_extension(format!("bak.tmp{}", std::process::id()));
if std::fs::copy(path, &bak_tmp).is_ok() {
if std::fs::rename(&bak_tmp, path.with_extension("bak")).is_err() {
let _ = std::fs::remove_file(&bak_tmp);
}
} else {
let _ = std::fs::remove_file(&bak_tmp);
}
}
let mut last = String::new();
for attempt in 0..3 {
match std::fs::rename(&tmp, path) {
Ok(()) => return Ok(()),
Err(e) => {
last = e.to_string();
// ACCESS_DENIED / PermissionDenied is the scanner; anything
// else will not improve by waiting.
if e.kind() != std::io::ErrorKind::PermissionDenied {
break;
}
std::thread::sleep(Duration::from_millis(30 * (attempt + 1)));
}
}
}
let _ = std::fs::remove_file(&tmp);
Err(format!("could not replace {} after 3 attempts: {last}", path.display()))
}
// ── process-wide state ──────────────────────────────────────────────────────
static STORE: Mutex<Option<LaunchStore>> = Mutex::new(None);
static SUPERVISOR: AtomicBool = AtomicBool::new(false);
/// The sldworks pid whose crash reporter this bridge has already clicked.
/// Per-PROCESS, not per-attempt: a retry must not re-arm the click.
static CLICKED_FOR: Mutex<Option<u32>> = Mutex::new(None);
/// Read the store WITHOUT writing it back.
///
/// The first version saved on every read, so `swx_status` — the verb ab tells
/// callers to POLL — rewrote `launch-state.json` a few times a second, for no
/// change, on the one file whose integrity the in-flight guard depends on.
fn read_store<T>(f: impl FnOnce(&LaunchStore) -> T) -> T {
let mut guard = STORE.lock().unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
*guard = Some(load());
}
f(guard.as_ref().expect("initialised above"))
}
/// Change the store and persist it. The ONLY path that writes.
fn mutate_store<T>(f: impl FnOnce(&mut LaunchStore) -> T) -> T {
let mut guard = STORE.lock().unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
*guard = Some(load());
}
let out = f(guard.as_mut().expect("initialised above"));
save(guard.as_ref().expect("initialised above"));
out
}
/// The route that last worked on this machine, if any.
pub fn preferred_route() -> Option<String> {
read_store(|s| s.preferred_route.clone())
}
/// The current attempt, if there is one.
pub fn attempt() -> Option<Attempt> {
read_store(|s| s.attempt.clone())
}
/// Is a launch genuinely in flight RIGHT NOW?
///
/// Three questions, in order, and the last two are what make this honest rather
/// than a timer:
///
/// 1. is THIS PROCESS supervising it? (the common case, and free)
/// 2. is the process that recorded it still alive? An attempt whose
/// `supervisorPid` is absent, is not us, and is not running belongs to a
/// bridge that is GONE — it is marked `abandoned` on the spot, so it stops
/// being reported and stops blocking the next launch.
/// 3. is OUR OWN CHAIN still alive? Note what is NOT here: "is any
/// sldworks.exe running". The first version accepted that, so a bridge
/// restarted while the user's SOLIDWORKS sat at a sign-in reported a
/// launch in flight forever and told the caller to poll a supervisor that
/// did not exist.
pub fn in_flight(os: &dyn LaunchOs) -> Option<Attempt> {
let a = attempt()?;
if a.finished {
return None;
}
if SUPERVISOR.load(Ordering::SeqCst) {
return Some(a);
}
if a.age_secs() > inflight_budget().as_secs() {
abandon("the attempt outlived its in-flight budget with no supervisor in this process");
return None;
}
let ours = a.supervisor_pid == Some(os.self_pid());
if !ours {
let alive = a.supervisor_pid.and_then(|p| os.pid_alive(p));
if alive != Some(true) {
abandon(match a.supervisor_pid {
None => "the attempt records no supervising bridge process, so nothing is watching it",
Some(_) => "the bridge process that started this launch is gone",
});
return None;
}
}
// Another live bridge owns it (or ours restarted the supervisor): believe it
// only while OUR OWN recorded chain is still running.
let census = os.processes().ok()?;
let chain_alive = !a.chain.is_empty()
&& a.chain.iter().any(|c| census.iter().any(|p| p.same_process(c)));
if chain_alive {
Some(a)
} else {
abandon("no process from this attempt's own launcher chain is still running");
None
}
}
/// Mark the current attempt as belonging to nobody.
fn abandon(why: &str) {
mutate_store(|s| {
if let Some(a) = s.attempt.as_mut() {
if !a.finished {
a.finished = true;
a.outcome = Some("abandoned".into());
a.error = Some(why.to_string());
}
}
});
crate::ledger::record("solidworks_launch_abandoned", json!({"reason": why}));
}
/// May a new attempt start, or is the machine in backoff after a failure?
pub fn backoff_remaining() -> u64 {
read_store(|s| {
if s.consecutive_failures == 0 {
return 0;
}
let Some(at) = s.last_failure_at else { return 0 };
let wait = backoff_budget().as_secs() * (s.consecutive_failures.min(4) as u64);
wait.saturating_sub(epoch_secs().saturating_sub(at))
})
}
// ── the supervisor ──────────────────────────────────────────────────────────
/// What the supervisor needs: invoke a route, and probe for a COM server. Both
/// go through the com host — the invoke because a `.lnk` needs the shell (and
/// `ShellExecuteExW` needs the host's STA thread), the probe because the com
/// host is the only thing that speaks COM.
pub struct Channel {
pub com: Arc<crate::comhost::ComHost>,
}
impl Channel {
fn invoke(&self, shortcut: &str, route: &str) -> Result<Value, String> {
self.com
.call(
"launch_shortcut",
json!({"shortcut": shortcut, "route": route}),
Duration::from_secs(60),
)
.map_err(|e| e.message)
}
fn attached(&self) -> bool {
self.com
.call("start", json!({"visible": true, "launch": false}), Duration::from_secs(15))
.is_ok()
}
}
/// Why a launch cannot start right now.
pub enum BeginRefusal {
/// One is already running; report it, launch nothing.
InFlight(Attempt),
/// The machine failed recently; a full re-sweep would start four more
/// chains. `seconds` until it may try again.
Backoff { seconds: u64, attempt: Option<Attempt> },
}
/// Begin an attempt and hand it to a background supervisor.
pub fn begin(
channel: Channel,
shortcut: &str,
reason: &str,
caller: &str,
) -> Result<Attempt, BeginRefusal> {
let wait = backoff_remaining();
if wait > 0 {
return Err(BeginRefusal::Backoff { seconds: wait, attempt: attempt() });
}
if SUPERVISOR.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
return Err(BeginRefusal::InFlight(
attempt().unwrap_or_else(|| unfinished_placeholder(shortcut)),
));
}
let os = crate::launchos::os();
// AFTER A FAILURE, TRY ONE ROUTE — not the whole plan again. A caller that
// retries in a loop would otherwise start four launcher chains per retry,
// which is the exact failure this release exists to end.
let (plan, failures) = read_store(|s| {
let full = route_plan(s.preferred_route.as_deref());
if s.consecutive_failures == 0 {
(full, 0)
} else {
let idx = (s.consecutive_failures as usize) % full.len();
(vec![full[idx].clone()], s.consecutive_failures)
}
});
let started = mutate_store(|s| {
let id = s.attempt.as_ref().map(|a| a.id + 1).unwrap_or(1);
if let Some(old) = s.attempt.take() {
s.history.push(old.to_json());
}
let a = Attempt {
id,
started_at: epoch_secs(),
shortcut: shortcut.to_string(),
reason: reason.to_string(),
caller: caller.to_string(),
plan: plan.clone(),
route: plan.first().cloned().unwrap_or_default(),
invoked_at: 0,
stage: Stage::Invoked,
stage_at: epoch_secs(),
chain: Vec::new(),
baseline_pids: Vec::new(),
baseline_known: false,
attribution: None,
launcher_log: None,
run_begin_seen: false,
sign_in_seen: false,
sign_in_window: None,
sign_in_inventory: None,
sign_in_wake: None,
sign_in_rejected: false,
sign_in_auto: false,
sign_in_auto_tried: false,
superseded_by_attach: None,
possibly_ours: Vec::new(),
chain_last_seen: Vec::new(),
attributed_sldworks: None,
attributed_how: None,
root_confirmed: false,
unattributed_log: false,
foreign_chain_seen: false,
last_route_before_attach: None,
tries: Vec::new(),
crash_recovery_clicked: false,
supervisor_pid: Some(os.self_pid()),
finished: false,
outcome: None,
error: None,
os_kind: os.kind(),
};
s.attempt = Some(a.clone());
a
});
crate::ledger::record(
"solidworks_launch_started",
json!({
"attempt": started.id,
"launchRoute": "platformShortcut",
"routePlan": started.plan,
"afterFailures": failures,
"shortcut": shortcut,
"reason": reason,
"caller": caller,
}),
);
let spawned = std::thread::Builder::new()
.name("swx-launch-supervisor".into())
.spawn(move || {
run_supervisor(channel, os);
SUPERVISOR.store(false, Ordering::SeqCst);
});
if spawned.is_err() {
SUPERVISOR.store(false, Ordering::SeqCst);
finish("failed", Some("could not start the launch supervisor thread".into()));
}
Ok(started)
}
fn unfinished_placeholder(shortcut: &str) -> Attempt {
Attempt {
id: 0,
started_at: epoch_secs(),
shortcut: shortcut.to_string(),
reason: String::new(),
caller: String::new(),
plan: Vec::new(),
route: String::new(),
invoked_at: 0,
stage: Stage::Invoked,
stage_at: epoch_secs(),
chain: Vec::new(),
baseline_pids: Vec::new(),
baseline_known: false,
attribution: None,
launcher_log: None,
run_begin_seen: false,
sign_in_seen: false,
sign_in_window: None,
sign_in_inventory: None,
sign_in_wake: None,
sign_in_rejected: false,
sign_in_auto: false,
sign_in_auto_tried: false,
superseded_by_attach: None,
possibly_ours: Vec::new(),
chain_last_seen: Vec::new(),
attributed_sldworks: None,
attributed_how: None,
root_confirmed: false,
unattributed_log: false,
foreign_chain_seen: false,
last_route_before_attach: None,
tries: Vec::new(),
crash_recovery_clicked: false,
supervisor_pid: Some(std::process::id()),
finished: false,
outcome: None,
error: None,
os_kind: "unknown",
}
}
fn update(f: impl FnOnce(&mut Attempt)) {
mutate_store(|s| {
if let Some(a) = s.attempt.as_mut() {
f(a);
}
});
}
fn set_stage(stage: Stage) {
let mut changed = false;
update(|a| {
if a.stage != stage {
a.stage = stage;
a.stage_at = epoch_secs();
changed = true;
}
});
if changed {
crate::ledger::record("solidworks_launch_stage", json!({"stage": stage.name()}));
}
}
fn finish(outcome: &str, error: Option<String>) {
let mut already = false;
mutate_store(|s| {
if let Some(a) = s.attempt.as_mut() {
// IDEMPOTENT. A second call must not re-decide the outcome, and
// above all must not bump `consecutive_failures` a second time for
// one attempt — which would double the backoff for free.
if a.finished {
already = true;
return;
}
a.finished = true;
a.outcome = Some(outcome.to_string());
a.error = error.clone();
if outcome == "attached" {
// MEMORISE THE ROUTE ONLY IF IT DID THE WORK. A user who starts
// SOLIDWORKS from their Start Menu mid-flight makes the attach
// succeed; recording the route we happened to be trying as
// "the one that works" would teach the bridge the wrong lesson
// from somebody else's click. `Run Begin` in OUR OWN launcher
// log is the evidence that OUR route started the application.
if a.run_begin_seen {
s.preferred_route = Some(a.route.clone());
}
s.consecutive_failures = 0;
s.last_failure_at = None;
} else {
s.consecutive_failures = s.consecutive_failures.saturating_add(1);
s.last_failure_at = Some(epoch_secs());
}
}
});
if !already {
crate::ledger::record(
"solidworks_launch_finished",
json!({"outcome": outcome, "error": error}),
);
}
}
/// Mark the current attempt done because SOLIDWORKS is attached.
pub fn mark_attached() {
let running = read_store(|s| s.attempt.as_ref().map(|a| !a.finished).unwrap_or(false));
if running {
set_stage(Stage::Attached);
finish("attached", None);
}
}
/// THE STATE MACHINE. One route at a time; a stall ends the route, never the
/// attempt, and never anything that was running before we started.
fn run_supervisor(channel: Channel, os: Box<dyn LaunchOs>) {
let plan = attempt().map(|a| a.plan).unwrap_or_default();
let shortcut = attempt().map(|a| a.shortcut).unwrap_or_default();
// WHAT RAN BEFORE THIS LAUNCH BEGAN — the FIRST route's baseline. Any
// sldworks.exe outside it is this launch's, whichever route started it.
let first_baseline: Vec<u32> = os.processes().map(|c| c.iter().map(|p| p.pid).collect()).unwrap_or_default();
// THE PERFORMANCE LOG AS NO SOLIDWORKS IS WRITING IT (0.11.3): a marker the
// session this launch starts writes must lie beyond this (or the file be
// rewritten) — see perflog::Baseline.
if os.processes().map(|c| !c.iter().any(|p| p.is("SLDWORKS.exe"))).unwrap_or(false) {
crate::perflog::record_baseline("launch", None);
}
for (index, route) in plan.iter().enumerate() {
// NEVER RE-INVOKE OVER A RUNNING SOLIDWORKS (0.11.2). Before a retry,
// watch a moment for a SOLIDWORKS the stalled route started anyway; if
// one is there, wait for it instead.
if index > 0 {
let until = std::time::Instant::now() + retry_settle();
loop {
if let Some(sw) = os.processes().ok().and_then(|c| new_solidworks(&c, &first_baseline)) {
crate::ledger::record(
"launch_retry_declined",
json!({"route": route, "why": format!("a SOLIDWORKS from this launch is already running (pid {}, createdAt {:?})", sw.pid, sw.created_at),
"pid": sw.pid, "createdAt": sw.created_at}),
);
wait_for_new_solidworks(&channel, sw.pid);
return;
}
if std::time::Instant::now() >= until {
break;
}
std::thread::sleep(poll_interval());
}
}
// EVERY INVOKE IS A "VERB" FOR THE DIALOG CENSUS (0.11.2): a dialog
// that appears while this route runs — or within the grace after it —
// is attributed to it, and a File ▸ Open with an empty name that it
// raised is cancelled by it.
let _dialogs = crate::dialogs::LaunchBracket::begin(if index == 0 { "launch.invoke" } else { "launch.retry" }, dialog_grace());
// EVERY PER-ROUTE FACT RESETS. `root_confirmed` and `unattributed_log`
// did not, with two consequences: `notConfirmed` became unreachable
// after route 1 (so H2 was restored for routes 2-4), and the log gate's
// root-confirmed half was pre-satisfied by an EARLIER route's chain.
update(|a| {
a.route = route.clone();
a.chain.clear();
a.launcher_log = None;
a.run_begin_seen = false;
a.root_confirmed = false;
a.unattributed_log = false;
a.foreign_chain_seen = false;
});
set_stage(Stage::Invoked);
// THE BASELINE, taken immediately BEFORE the invoke. Everything
// terminable that is already running is recorded as NOT OURS, whatever
// its creation time says afterwards.
let baseline = os.processes();
let baseline_pids: Vec<u32> = baseline
.as_ref()
.map(|c| c.iter().map(|p| p.pid).collect())
.unwrap_or_default();
let baseline_known = baseline.is_ok();
// THE LOG BASELINE, and whether we could take one at all. A FAILED
// listing is UNKNOWN — `unwrap_or_default()` turned it into "there were
// no logs", and an absent baseline used to make ANY log ours. Five of
// the nine logs on the real machine contain `Run Begin`, so that read
// somebody else's launch from three days ago and jumped the machine
// straight past stall detection.
let logs = os.launcher_logs();
let log_baseline_known = logs.is_ok();
let (base_stamp, base_created) = newest_log(logs.as_deref().unwrap_or(&[]));
let invoked_at = os.now();
update(|a| {
a.invoked_at = invoked_at;
a.baseline_pids = baseline_pids.clone();
a.baseline_known = baseline_known;
a.attribution = None;
});
let invoked = match channel.invoke(&shortcut, route) {
Err(e) => {
record_try(
route,
invoked_at,
false,
Cleanup { kind: "nothingToClean", ..Default::default() },
Some(format!("the invoke itself failed: {e}")),
);
crate::ledger::record(
"solidworks_launch_route_failed",
json!({"route": route, "error": e}),
);
continue;
}
Ok(r) => {
if r.get("alreadyRunning").and_then(|v| v.as_bool()).unwrap_or(false)
|| r.get("attached").and_then(|v| v.as_bool()).unwrap_or(false)
{
set_stage(Stage::Attached);
finish("attached", None);
return;
}
r
}
};
// WHAT DID THE INVOKE SAY IT STARTED? This, and nothing found by
// searching the machine afterwards, is authority to terminate.
let attribution = invoked
.get("rootPid")
.and_then(|v| v.as_u64())
.filter(|p| *p > 0)
.map(|pid| Attribution {
pid: pid as u32,
created_at: invoked.get("rootCreatedAt").and_then(|v| v.as_u64()),
source: invoked
.get("attributionSource")
.and_then(|v| v.as_str())
.unwrap_or("invoke")
.to_string(),
})
// An attribution with no creation time is not an identity, and this
// bridge will not terminate a bare number.
.filter(|a| a.created_at.is_some());
update(|a| a.attribution = attribution.clone());
if attribution.is_none() {
crate::ledger::record(
"solidworks_launch_not_attributable",
json!({
"route": route,
"why": invoked.get("attributionNote").cloned().unwrap_or(Value::Null),
"consequence": "no process from this route will be terminated by the bridge",
}),
);
}
let watched = Watch {
baseline_pids,
base_stamp,
base_created,
log_baseline_known,
invoked_at,
attribution: attribution.clone(),
};
match watch_route(&channel, os.as_ref(), route, &watched) {
RouteOutcome::Attached => {
// The WINNING route belongs in the per-route table too, or the
// gate's central measurement is missing its most important row.
record_try(
route,
invoked_at,
false,
Cleanup { attributed: attribution.is_some(), kind: "nothingToClean", ..Default::default() },
Some(format!("route '{route}' ended with SOLIDWORKS COM-attached")),
);
// AN ORDERING HINT, NOT A PROOF. Which route was in flight when
// the attach happened is worth recording — but it is not
// evidence that this route did it (the user may have started
// SOLIDWORKS themselves), so it never satisfies `run_begin_seen`
// and is never promoted to `preferredRoute`.
let last = route.clone();
mutate_store(|st| {
if let Some(a) = st.attempt.as_mut() {
a.last_route_before_attach = Some(last.clone());
}
});
finish("attached", None);
return;
}
RouteOutcome::Stop => {
// A STOPPED ROUTE BELONGS IN THE TABLE TOO. `Stop` is the
// `sldworksUpNotAttached` ending — SOLIDWORKS is on the user's
// screen and COM has not answered, which on this edition usually
// means a sign-in only they can clear. It recorded NO row at all,
// so `routesTried` came back empty for a route that had reached
// Run Begin and started the application; the attempt-level
// `runBeginSeen:true` sat beside an empty table, and
// `reattribute_after_attach` — which looks for the route that
// proved itself — found nothing to point at. The most important
// outcome for sign-in recovery was the one with no evidence row.
record_try(
route,
invoked_at,
false,
Cleanup {
attributed: attribution.is_some(),
kind: "nothingToClean",
..Default::default()
},
Some(format!(
"route '{route}' started SOLIDWORKS and stopped: the application is up and \
COM has not attached. Nothing was cleaned up — the chain did its job."
)),
);
return;
}
RouteOutcome::Stalled { note, saw_log, unattributed_log } => {
// A SOLIDWORKS THIS LAUNCH STARTED IS ALREADY UP: the "stall" is
// over. Its chain is not killed and nothing is invoked again.
if let Some(sw) = os.processes().ok().and_then(|c| new_solidworks(&c, &first_baseline)) {
crate::ledger::record(
"launch_retry_declined",
json!({"route": route, "note": note, "why": format!("a SOLIDWORKS from this launch is already running (pid {}, createdAt {:?})", sw.pid, sw.created_at),
"pid": sw.pid, "createdAt": sw.created_at}),
);
wait_for_new_solidworks(&channel, sw.pid);
return;
}
// THE FOUR WAYS A ROUTE CAN END, and they are not the same.
// * attributed AND confirmed: the invoke told us its root and
// we saw it, so the chain is ours by proof and we end it;
// * attributed but NEVER CONFIRMED: we were told a root and
// never once saw it or anything descending from it, so we
// cannot say what is running;
// * blind: we could not even take a census when we invoked;
// * not attributable: the route ran but cannot say what it
// started, so nothing may be terminated on its behalf.
let confirmed = attempt().map(|a| a.root_confirmed).unwrap_or(false);
let cleanup = match (attribution.as_ref(), baseline_known) {
(Some(_), true) | (Some(_), false) if confirmed => {
clean_up_chain(os.as_ref(), invoked_at)
}
(Some(root), _) => Cleanup::not_confirmed(route, root),
(None, false) => Cleanup::blind(),
(None, true) => Cleanup::not_attributable(route, unattributed_log),
};
let note = if saw_log {
note
} else {
format!(
"route '{route}' produced NO launcher log at all within {}s: nothing got \
as far as writing one, because nothing started. The invocation returned \
successfully, which for a process spawn only means the EXE ran — it says \
nothing about whether the shell resolved the .lnk or the launcher chain \
came up.",
run_begin_budget().as_secs()
)
};
let failed = cleanup.failed;
let kind = cleanup.kind;
let error = cleanup.error.clone();
let killed: Vec<Value> = cleanup.killed.iter().map(|p| p.to_json()).collect();
let left = cleanup.left_running.len();
record_try(route, invoked_at, true, cleanup, Some(note.clone()));
set_stage(Stage::Stalled);
crate::ledger::record(
"solidworks_launch_stalled",
json!({
"route": route,
"note": note,
"killed": killed,
"cleanup": kind,
"cleanupFailed": failed,
"cleanupError": error,
"nextRoute": plan.get(index + 1),
}),
);
// MAY THE MACHINE GO ON? Only when nothing was left behind
// that it cannot account for. A route that started NOTHING (no
// launcher log ever appeared) leaves nothing behind whether or
// not it was attributable — that is what makes it safe to try
// the next one. A route that started something it cannot
// identify is where the machine stops: another route would put
// a second chain beside one nobody can find.
let may_continue = match kind {
"cleaned" | "nothingToClean" => true,
// A route that cannot say what it started may be followed
// by another ONLY if nothing appears to have started: no
// log of ours (there can be none) and no log we could not
// attribute either. A new log is evidence that SOMETHING
// ran, and we cannot say whose it is.
"notAttributable" => !saw_log && !unattributed_log,
_ => false,
};
if !may_continue {
set_stage(Stage::Exhausted);
finish(
"exhausted",
Some(format!(
"stopped after route '{route}' ({kind}): {}. This bridge will not \
start another launcher chain while processes it cannot account for \
may still be running. {left} launcher process(es) are known to be \
still running; ASK THE USER to start SOLIDWORKS from their Start \
Menu, which works when these routes do not.",
error.unwrap_or_else(|| "reason unknown".into())
)),
);
return;
}
}
}
}
set_stage(Stage::Exhausted);
finish(
"exhausted",
Some("every launch route was tried and none produced a COM-attachable SOLIDWORKS".into()),
);
}
/// A SOLIDWORKS from this launch is up: wait for COM to attach to IT, for the
/// attach budget, instead of starting anything else.
fn wait_for_new_solidworks(channel: &Channel, pid: u32) {
set_stage_at_least(Stage::SldworksUp);
set_stage_at_least(Stage::Attaching);
let until = std::time::Instant::now() + attach_budget();
while std::time::Instant::now() < until {
if channel.attached() {
set_stage(Stage::Attached);
finish("attached", None);
return;
}
std::thread::sleep(attach_poll_interval());
}
finish(
"sldworksUpNotAttached",
Some(format!(
"sldworks.exe (pid {pid}) from this launch is running but COM did not attach within {}s — it may be at a \
3DEXPERIENCE sign-in only the user can answer. Nothing was invoked again.",
attach_budget().as_secs()
)),
);
}
fn record_try(route: &str, invoked_at: u64, stalled: bool, c: Cleanup, note: Option<String>) {
update(|a| {
let stage = a.stage;
// The evidence is COPIED off the attempt HERE, at the end of the route,
// while it still describes this route and nothing else — every one of
// these fields is reset when the next route begins.
let (log, run_begin, unattributed, root_pid, root_confirmed) = (
a.launcher_log.clone(),
a.run_begin_seen,
a.unattributed_log,
a.attribution.as_ref().map(|r| r.pid),
a.root_confirmed,
);
a.tries.push(RouteTry {
route: route.to_string(),
invoked_at,
stage,
stalled,
killed: c.killed,
cleanup_failed: c.failed,
cleanup_error: c.error,
chain_left_running: c.left_running,
attributed: c.attributed,
cleanup: c.kind,
chain_may_remain: c.may_remain,
launcher_log: log,
run_begin_seen: run_begin,
unattributed_log: unattributed,
root_pid,
root_confirmed,
note,
});
});
}
/// WHICH OF THESE LOGS IS OURS? Three tests, in order of how much they prove.
///
/// `root.created_at <= written` — round four's rule — is NECESSARY AND NOT
/// SUFFICIENT: it is true of every log written after our chain started,
/// including one written by a launch the user began while ours was stalling.
/// And `launcher_logs()` is newest-first, so a bare `find` preferentially
/// picked THEIRS. Our own route's log stops at `SecurityContext:`; theirs has
/// `Run Begin` in it; the bridge reported that it had seen `Run Begin`.
///
/// 1. **AUTHORSHIP.** The launcher stamps the writing thread's id on every
/// line. If that thread belongs to a process in OUR OWN chain, the log is
/// ours — positively, not by elimination. (A miss proves nothing: threads
/// exit, and the writing one usually has.)
/// 2. **SOLE CANDIDATE.** Exactly one log post-dates the baseline, our chain
/// has been SEEN and existed before the log was written, and no launcher
/// chain that is not ours was alive in the meantime. Then there is nothing
/// else it could be.
/// 3. **Otherwise: none.** Two candidates and no authorship proof is
/// `unattributedLog`. Never "pick the newest".
fn choose_our_log(
os: &dyn LaunchOs,
candidates: &[&crate::launchos::LogFile],
w: &Watch,
chain_pids: &[u32],
foreign_chain_seen: bool,
root_confirmed: bool,
) -> Option<String> {
let root = w.attribution.as_ref()?;
// 1. AUTHORSHIP.
for l in candidates {
let Ok(text) = os.read_log(&l.path) else { continue };
let Some(tid) = crate::launchos::writer_thread_of(&text) else { continue };
let Some(owner) = os.thread_owner(tid) else { continue };
if owner == root.pid || chain_pids.contains(&owner) {
return Some(l.path.clone());
}
}
// 2. SOLE CANDIDATE, bounded from both sides — and only once our own chain
// has actually been SEEN. A root we were promised and never found is no
// evidence that anything of ours ran, so it cannot claim a log either.
if candidates.len() == 1 && !foreign_chain_seen && root_confirmed {
let l = candidates[0];
let born_before = match (root.created_at, l.created_at) {
(Some(born), Some(written)) => born <= written,
_ => false,
};
if born_before {
return Some(l.path.clone());
}
}
None
}
/// The pids currently recorded as our chain.
pub fn our_chain_pids() -> Vec<u32> {
chain_pids_now()
}
fn chain_pids_now() -> Vec<u32> {
attempt().map(|a| a.chain.iter().map(|p| p.pid).collect()).unwrap_or_default()
}
/// The newest (stamp, created_at) across a set of logs.
fn newest_log(logs: &[crate::launchos::LogFile]) -> (Option<u64>, Option<u64>) {
(
logs.iter().filter_map(|l| l.stamp).max(),
logs.iter().filter_map(|l| l.created_at).max(),
)
}
struct Watch {
baseline_pids: Vec<u32>,
base_stamp: Option<u64>,
base_created: Option<u64>,
/// Could the launcher-log directory be listed before we invoked? When it
/// could not, NO log is read at all — see `LogFile::is_newer_than`.
log_baseline_known: bool,
invoked_at: u64,
attribution: Option<Attribution>,
}
enum RouteOutcome {
/// COM attached. The only success.
Attached,
/// This route stalled; clean up its chain and try the next one. `saw_log`
/// records whether a launcher log for THIS invoke was ever found, so the
/// note can say what actually happened instead of assuming.
Stalled { note: String, saw_log: bool, unattributed_log: bool },
/// Terminal for the WHOLE attempt, already finished and recorded.
Stop,
}
/// Watch ONE route from its invoke to attach, a stall, or its budget.
fn watch_route(channel: &Channel, os: &dyn LaunchOs, route: &str, w: &Watch) -> RouteOutcome {
let started = std::time::Instant::now();
let mut last_attach_probe = std::time::Instant::now() - attach_poll_interval();
let mut seen_run_begin = false;
let mut seen_sldworks = false;
let mut saw_log = false;
let mut unattributed_log = false;
let mut root_confirmed = attempt().map(|a| a.root_confirmed).unwrap_or(false);
let mut log_baseline_unknown = false;
// WAS ANOTHER LAUNCH RUNNING? A terminable-image process that is neither in
// our chain nor in the baseline is somebody else's launcher chain, and its
// existence is what makes "the only new log must be ours" stop being true.
let mut foreign_chain_seen = false;
let mut sldworks_pid: Option<u32> = None;
// ── THE SIGN-IN PAUSE. Time spent with a 3DEXPERIENCE ID window on screen
// does not count against any launch deadline: the chain is not stalled,
// it is waiting for a person. `sign_in_since` is the current wait,
// `signed_in_paused` the total of the ones that ended, and
// `sign_in_unreadable` records that we could not even ask — which pauses
// too, because resuming a deadline on an unreadable census is how the
// machine killed the window it could not see.
let mut sign_in_since: Option<std::time::Instant> = None;
let mut signed_in_paused = Duration::ZERO;
let mut sign_in_announced = false;
let mut sign_in_hwnd: Option<i64> = None;
// ── THE HELPER SIGN-IN PAUSE (0.11.2). From the helper's log appearing
// until sldworks.exe does, the stall clocks STOP: that stage can take
// minutes (and may be waiting on the user). Only a DEAD chain ends it.
let mut helper_since: Option<std::time::Instant> = None;
let mut helper_paused = Duration::ZERO;
let mut chain_alive = true;
loop {
if last_attach_probe.elapsed() >= attach_poll_interval() {
last_attach_probe = std::time::Instant::now();
if channel.attached() {
set_stage(Stage::Attached);
return RouteOutcome::Attached;
}
}
// THE LAUNCHER'S OWN LOG. Read only when we had a BASELINE to compare
// against: without one, "is this log ours?" has no answer, and the old
// code answered "yes, all of them".
if !w.log_baseline_known {
log_baseline_unknown = true;
} else if let Ok(logs) = os.launcher_logs() {
// A VERIFIABLY EMPTY directory and a FAILED listing both produce no
// baseline stamps, and they are opposite facts. `is_newer_than`
// refuses the second by refusing an absent baseline, so the first is
// recognised HERE — where we know the listing succeeded.
let baseline_was_empty = w.base_stamp.is_none() && w.base_created.is_none();
let candidates: Vec<&crate::launchos::LogFile> = logs
.iter()
.filter(|l| baseline_was_empty || l.is_newer_than(w.base_stamp, w.base_created))
.collect();
// Once a log is OURS it stays ours: re-selecting on every poll is
// how a log that appears later (a second launch) displaces the one
// we were reading.
let already: Option<String> = attempt().and_then(|a| a.launcher_log);
let chosen: Option<String> = match &already {
Some(path) if candidates.iter().any(|l| &l.path == path) => Some(path.clone()),
_ => choose_our_log(
os,
&candidates,
w,
&chain_pids_now(),
foreign_chain_seen,
root_confirmed,
),
};
match chosen {
Some(path) => {
saw_log = true;
update(|a| a.launcher_log = Some(path.clone()));
set_stage_at_least(Stage::LauncherStarted);
if let Ok(text) = os.read_log(&path) {
if text.contains("Run Begin") {
seen_run_begin = true;
update(|a| a.run_begin_seen = true);
set_stage_at_least(Stage::RunBegin);
}
}
}
None if !candidates.is_empty() && !unattributed_log => {
unattributed_log = true;
update(|a| a.unattributed_log = true);
crate::ledger::record(
"solidworks_launch_unattributed_log",
json!({
"route": route,
"candidates": candidates.iter().map(|l| l.path.clone()).collect::<Vec<_>>(),
"why": if w.attribution.is_none() {
"this route does not report what it started, so no launcher log \
can be attributed to it"
} else if candidates.len() > 1 {
"more than one launcher log post-dates this invoke and none could \
be tied to a process in our own chain — picking the newest would \
be a guess, and on this machine the newest is as likely to be \
somebody else's launch as ours"
} else {
"no process from this route's own chain has been confirmed, or \
another launcher chain was running, so nothing proves this log \
is ours"
},
"consequence": "it is NOT read as this bridge's own evidence; no stage \
transition and no route memory comes from it",
}),
);
}
None => {}
}
}
if seen_run_begin && w.log_baseline_known {
let baseline_was_empty = w.base_stamp.is_none() && w.base_created.is_none();
if let Ok(logs) = os.helper_logs() {
if logs
.iter()
.any(|l| baseline_was_empty || l.is_newer_than(w.base_stamp, w.base_created))
{
set_stage_at_least(Stage::HelperSignIn);
if helper_since.is_none() && !seen_sldworks && helper_paused.is_zero() {
helper_since = Some(std::time::Instant::now());
}
}
}
}
// THE PROCESSES. Our chain is recorded BY IDENTITY, grown from the
// pids we already know are ours — never re-derived from the clock.
if let Ok(census) = os.processes() {
// ── RECORD THE CHAIN FIRST, THEN JUDGE WHAT IS FOREIGN. The order
// was the other way round, and it made a launch call ITSELF
// foreign: on the first census after an invoke the recorded chain
// is still EMPTY and only the attribution ROOT (CATSTART) was
// excluded by pid — so our own `SWXDesktopLauncher`, the root's
// own child, matched "terminable, not ours, not baseline, not the
// root" and latched `foreignChainSeen` on every ordinary run.
//
// That is not cosmetic. `reattribute_after_attach` bails on
// `foreign_chain_seen` BEFORE it ever reaches the lineage proof,
// so sign-in recovery — the whole point of 0.6.0 — was inert. It
// is the `foreignChainSeen:true` seen live on 2026-09-11.
//
// THE CHAIN, ONLY FROM THE ATTRIBUTED ROOT. `attributed_chain`
// returns None when the invoke could not say what it started, and
// None means we record nothing — not "look around and guess".
let recorded: Vec<ProcInfo> = attempt().map(|a| a.chain).unwrap_or_default();
if let Some(mine) =
attributed_chain(&census, w.attribution.as_ref(), &recorded, &w.baseline_pids, w.invoked_at)
{
if !mine.is_empty() {
// SEEING THE CHAIN AT LEAST ONCE is what makes an attributed
// route accountable: it is the difference between "there was
// nothing left to kill" and "I never found what I started".
root_confirmed = true;
// ...and WHEN we saw each of them, which is what bounds the
// window a later process may claim one of them as its parent.
let now = crate::launchos::epoch_secs();
let seen: Vec<(u32, u64, u64)> = mine
.iter()
.filter_map(|p| p.created_at.map(|c| (p.pid, c, now)))
.collect();
update(|a| {
a.chain = mine;
a.root_confirmed = true;
for (pid, created, at) in &seen {
match a
.chain_last_seen
.iter_mut()
.find(|(p, c, _)| p == pid && c == created)
{
Some(row) => row.2 = *at,
None => a.chain_last_seen.push((*pid, *created, *at)),
}
}
});
}
}
// ...and only now ask what is somebody else's, against the chain as
// it stands AFTER this census — plus every descendant of our own
// root, because the launcher IS our chain whether or not the walk has
// caught up with it yet.
let ours: Vec<u32> = chain_pids_now();
let root_pids: Vec<u32> = w.attribution.as_ref().map(|r| vec![r.pid]).unwrap_or_default();
let descendants: Vec<u32> = if root_pids.is_empty() {
Vec::new()
} else {
chain_candidates(&census, &w.baseline_pids, &root_pids, w.invoked_at)
.into_iter()
.map(|p| p.pid)
.collect()
};
// ── FOREIGNNESS NEEDS POSITIVE EVIDENCE, and an UNATTRIBUTABLE route
// supplies none either way. `explorer` and `cmdStart` cannot say
// what they started, so `root_pids` and `descendants` are both
// empty and their own launcher chains matched every clause of this
// test — the machine called the processes it had just started
// somebody else's, on a machine whose only launcher processes were
// the ones it started. Same shape as the 0.6.3 finding, one route
// further down the ladder.
//
// A launcher process that appeared AFTER our invoke, is not in the
// baseline, and belongs to a route that cannot name its own root is
// UNATTRIBUTED — POSSIBLY OURS. It is recorded as such and excluded
// from the foreign set, and it never enters the kill path: an
// unattributable route still cleans up nothing, which is the whole
// point of the attribution law. "Possibly ours" is a reason NOT to
// kill and NOT to accuse, which are the same answer here.
let mut possibly_ours: Vec<u32> = Vec::new();
let foreign: Vec<&ProcInfo> = census
.iter()
.filter(|p| {
TERMINABLE_IMAGES.iter().any(|t| t.eq_ignore_ascii_case(&p.image))
&& !ours.contains(&p.pid)
&& !w.baseline_pids.contains(&p.pid)
&& !root_pids.contains(&p.pid)
&& !descendants.contains(&p.pid)
})
.filter(|p| {
// POSITIVE EVIDENCE that it is not ours: it predates our
// invoke (so no invoke of ours could have started it). A
// launcher that appeared after our invoke, while a route of
// ours was running that cannot name what it started, is not
// evidence of anybody else.
let predates = p.created_at.map(|c| c + 1 < w.invoked_at).unwrap_or(false);
if !predates {
possibly_ours.push(p.pid);
}
predates
})
.collect();
if !possibly_ours.is_empty() {
let seen = possibly_ours.clone();
update(|a| {
for pid in &seen {
if !a.possibly_ours.contains(pid) {
a.possibly_ours.push(*pid);
}
}
});
}
if !foreign.is_empty() && !foreign_chain_seen {
let names: Vec<String> =
foreign.iter().map(|p| format!("{} (pid {})", p.image, p.pid)).collect();
foreign_chain_seen = true;
update(|a| a.foreign_chain_seen = true);
crate::ledger::record(
"solidworks_launch_foreign_chain",
json!({
"processes": names,
"why": "these launcher processes PREDATE this bridge's invoke, so no route \
of ours started them — somebody else's launch is running",
}),
);
}
if let Some(sw) = census.iter().find(|p| p.is("SLDWORKS.exe")) {
seen_sldworks = true;
sldworks_pid = Some(sw.pid);
set_stage_at_least(Stage::SldworksUp);
set_stage_at_least(Stage::Attaching);
// The helper stage is over: the clocks resume.
if let Some(t) = helper_since.take() {
helper_paused += t.elapsed();
}
}
// IS OUR CHAIN STILL THERE? Only a chain we have SEEN can be dead.
let ours_now = chain_pids_now();
chain_alive = !root_confirmed || ours_now.is_empty() || census.iter().any(|p| ours_now.contains(&p.pid));
}
if seen_run_begin {
recover_crash_reporter(sldworks_pid);
}
// ── IS THE USER BEING ASKED TO SIGN IN? Asked of OUR chain's pids only,
// and only once the chain exists to ask about.
{
let ours = chain_pids_now();
let found = if ours.is_empty() {
Some(None)
} else {
sign_in_window(os, &ours)
};
match found {
// A window is up, or the census could not be read while our chain
// is alive. Both pause.
Some(Some((hwnd, title))) => {
if sign_in_since.is_none() {
{
let t = title.clone();
update(move |a| a.sign_in_window = Some(t));
}
// INVENTORY FIRST, every time (0.9.13) — and the
// unattended sign-in when the caller armed it.
on_sign_in_window(hwnd);
sign_in_since = Some(std::time::Instant::now());
sign_in_hwnd = Some(hwnd);
set_stage_at_least(Stage::HelperSignIn);
update(|a| a.sign_in_seen = true);
crate::ledger::record(
"solidworks_launch_sign_in",
json!({
"route": route,
"hwnd": hwnd,
"title": title,
"effect": "every launch deadline is PAUSED while this window is on \
screen; nothing in this chain will be killed or \
escalated. Only the user can answer it.",
}),
);
if !sign_in_announced {
sign_in_announced = true;
announce_sign_in(&title);
}
}
}
None => {
// Census unreadable for every pid we hold. Pause rather than
// assume the screen is clear.
if sign_in_since.is_none() {
sign_in_since = Some(std::time::Instant::now());
}
}
Some(None) => {
if let Some(t) = sign_in_since.take() {
update(|a| a.sign_in_window = None);
signed_in_paused += t.elapsed();
crate::ledger::record(
"solidworks_launch_sign_in_done",
json!({
"route": route,
"hwnd": sign_in_hwnd,
"waitedSeconds": t.elapsed().as_secs(),
"effect": "the sign-in window is gone; the launch deadlines resume \
from where they were paused",
}),
);
}
}
}
}
// EVERY DEADLINE BELOW is measured on this clock, which does not run
// while a human is being asked for a password.
let elapsed = started
.elapsed()
.saturating_sub(signed_in_paused + sign_in_since.map(|t| t.elapsed()).unwrap_or_default())
.saturating_sub(helper_paused + helper_since.map(|t| t.elapsed()).unwrap_or_default());
// THE HELPER STAGE: not a stall while the chain lives. A dead chain is
// one; a helper that outlasts its own (long) budget STOPS the launch —
// it is never retried, because a retry is a second invoke at whatever
// the first one is still bringing up.
if let Some(t) = helper_since {
if !chain_alive {
return RouteOutcome::Stalled {
note: format!("route '{route}' reached the 3DEXPERIENCE helper sign-in and its launcher chain then EXITED with no sldworks.exe"),
saw_log,
unattributed_log,
};
}
if t.elapsed() > helper_budget() {
finish(
"helperSignInTooLong",
Some(format!(
"the 3DEXPERIENCE helper sign-in has run {}s with no sldworks.exe — it may be waiting on the user. \
Nothing was killed and the shortcut was NOT invoked again (a second invoke reaches a starting \
SOLIDWORKS as an 'open' request: a File ▸ Open dialog, measured 2026-10-15)",
t.elapsed().as_secs()
)),
);
return RouteOutcome::Stop;
}
}
if !seen_run_begin && elapsed > run_begin_budget() {
if log_baseline_unknown {
// NO EVIDENCE EITHER WAY. The launcher-log directory could not
// be listed before the invoke, so no log can be attributed to
// this launch — and a stall is a statement ABOUT a log.
return RouteOutcome::Stalled {
note: format!(
"route '{route}' could not be judged: the launcher's log directory could \
not be listed before the invoke, so no log can be attributed to this \
launch. NOTHING is concluded about whether the chain started, reached \
SecurityContext or stalled."
),
saw_log: true, // suppress the "nothing started" note: we do not know that
unattributed_log,
};
}
return RouteOutcome::Stalled {
note: format!(
"route '{route}' started a launcher chain that reached SecurityContext but \
never logged 'Run Begin' within {}s",
run_begin_budget().as_secs()
),
saw_log,
unattributed_log,
};
}
if seen_run_begin && !seen_sldworks && elapsed > sldworks_budget() {
return RouteOutcome::Stalled {
note: format!(
"route '{route}' logged 'Run Begin' but no sldworks.exe appeared within {}s",
sldworks_budget().as_secs()
),
saw_log,
unattributed_log,
};
}
if elapsed > attach_budget() {
// SOLIDWORKS IS UP AND NOT ANSWERING COM. Not a stall of the
// launcher chain, and the chain must NOT be killed for it: the
// application is on the user's screen, possibly at a sign-in only
// they can answer. Stop; do not try another route.
if seen_sldworks {
set_stage_at_least(Stage::Attaching);
finish(
"sldworksUpNotAttached",
Some(format!(
"sldworks.exe is running but COM did not attach within {}s — it may be at a \
3DEXPERIENCE sign-in only the user can answer",
attach_budget().as_secs()
)),
);
return RouteOutcome::Stop;
}
return RouteOutcome::Stalled {
note: format!(
"route '{route}' produced no COM server within {}s",
attach_budget().as_secs()
),
saw_log,
unattributed_log,
};
}
std::thread::sleep(poll_interval());
}
}
fn set_stage_at_least(stage: Stage) {
let current = attempt().map(|a| a.stage).unwrap_or(Stage::Invoked);
if current < stage && current < Stage::Attached {
set_stage(stage);
}
}
/// What a cleanup did, and what it could not do.
#[derive(Default)]
struct Cleanup {
killed: Vec<ProcInfo>,
left_running: Vec<ProcInfo>,
failed: bool,
error: Option<String>,
attributed: bool,
kind: &'static str,
may_remain: bool,
}
impl Cleanup {
/// The route could not say what it started, so nothing may be terminated.
/// This is NOT a failure of the cleanup — it is a property of the route,
/// and the difference matters to whoever reads the report.
fn not_attributable(route: &str, a_log_appeared: bool) -> Cleanup {
Cleanup {
kind: "notAttributable",
may_remain: a_log_appeared,
error: Some(format!(
"route '{route}' does not report what it started (the shell forwards the link and \
returns nothing), so this bridge cannot prove which launcher chain is its own \
and will not terminate any. {} The user can end any stray chain from Task \
Manager, and starting SOLIDWORKS from their Start Menu is unaffected by it.",
if a_log_appeared {
"A launcher log WAS written while this route was watched, so a \
CATSTART/SWXDesktopLauncher chain from this attempt MAY still be running."
} else {
"No launcher log appeared at all, so most likely nothing started."
}
)),
..Default::default()
}
}
/// The invoke DID hand back a root, and across the whole watch neither that
/// process nor anything descending from it was ever seen. We cannot say
/// whether the chain we started is still running, which is a failure to
/// account for it — not a clean "there was nothing to kill".
fn not_confirmed(route: &str, root: &Attribution) -> Cleanup {
Cleanup {
kind: "notConfirmed",
failed: true,
attributed: true,
may_remain: true,
error: Some(format!(
"route '{route}' reported starting pid {} ({}), and neither that process nor any \
descendant of it was ever seen in the process census. This bridge cannot tell \
whether the chain it started is still running, so it stopped rather than \
starting another.",
root.pid, root.source
)),
..Default::default()
}
}
/// The baseline census failed, so this bridge cannot see the machine it is
/// starting processes on.
fn blind() -> Cleanup {
Cleanup {
kind: "failed",
failed: true,
error: Some(
"the process census could not be taken when this route was invoked, so nothing \
could be attributed and nothing could be cleaned up. Any launcher chain this \
attempt started is still running and this bridge cannot identify it."
.to_string(),
),
..Default::default()
}
}
}
/// End OUR OWN launcher chain — the processes recorded on the attempt, and
/// nothing else.
///
/// The list is NOT re-derived from the census here. It was, and that is how a
/// launch the user started during ours ended up in it: everything terminable
/// younger than our invoke was swept in, whoever started it. Now each recorded
/// process is re-confirmed as the SAME process (pid + image + creation time)
/// and then killed through a handle that verifies the same thing again.
fn clean_up_chain(os: &dyn LaunchOs, invoked_at: u64) -> Cleanup {
let recorded = attempt().map(|a| a.chain).unwrap_or_default();
if recorded.is_empty() {
// Nothing was ever attributed to us. If the census is working, that is
// a genuine "nothing to clean"; if it is not, we cannot say.
return match os.processes() {
Ok(_) => Cleanup { attributed: true, kind: "nothingToClean", ..Default::default() },
Err(e) => Cleanup {
failed: true,
attributed: true,
kind: "failed",
error: Some(format!(
"the process census failed ({e}), so this bridge cannot tell whether the \
launcher chain it started is still running"
)),
..Default::default()
},
};
}
let census = match os.processes() {
Ok(c) => c,
Err(e) => {
crate::ledger::record(
"solidworks_launch_cleanup_skipped",
json!({"reason": format!("the process census failed: {e}")}),
);
return Cleanup {
left_running: recorded,
failed: true,
attributed: true,
kind: "failed",
error: Some(format!("the process census failed: {e}")),
..Default::default()
};
}
};
let mut killed = Vec::new();
let mut left = Vec::new();
let mut errors: Vec<String> = Vec::new();
for p in recorded {
// Still the same process? A pid alone is not an identity.
let Some(live) = census.iter().find(|c| c.same_process(&p)) else {
continue; // it exited on its own, or the number belongs to somebody else now
};
if !may_terminate(&live.image, live.created_at, invoked_at) {
left.push(live.clone());
errors.push(format!("pid {} ({}) is not on the kill allowlist", live.pid, live.image));
continue;
}
match os.terminate(live.pid, &live.image, live.created_at) {
Ok(()) => killed.push(live.clone()),
Err(e) => {
left.push(live.clone());
errors.push(format!("pid {}: {e}", live.pid));
crate::ledger::record(
"solidworks_launch_cleanup_failed",
json!({"pid": live.pid, "image": live.image, "error": e}),
);
}
}
}
Cleanup {
failed: !left.is_empty(),
kind: if left.is_empty() { "cleaned" } else { "failed" },
attributed: true,
may_remain: !left.is_empty(),
error: (!errors.is_empty()).then(|| errors.join("; ")),
killed,
left_running: left,
}
}
// ── crash-reporter recovery ─────────────────────────────────────────────────
/// If the SOLIDWORKS crash reporter is on screen with a "Restart SOLIDWORKS
/// Design" button, click it ONCE and let the state machine carry on.
///
/// OFF BY DEFAULT (`SWX_CRASH_RECOVERY=on`). The decision to click rests
/// entirely on the shape of ab's `desktop_find_control` reply, and that shape
/// has never been observed on a real desktop — see docs/LAUNCH-RESILIENCE.md
/// §9. Shipping it armed would mean guessing about a click on somebody's screen.
///
/// The rules, when it IS armed:
/// * the window must be OWNED BY the SOLIDWORKS process we are watching —
/// a title is not attribution;
/// * ab must report the button STRUCTURALLY (see `mentions_button`);
/// * once per attempt AND once per bridge process per sldworks pid, so a
/// retry cannot re-arm it;
/// * nothing else is ever clicked.
pub fn recover_crash_reporter(sldworks_pid: Option<u32>) -> Option<Value> {
if !crash_recovery_enabled() {
return None;
}
if attempt().map(|a| a.crash_recovery_clicked).unwrap_or(true) {
return None;
}
let pid = sldworks_pid?;
{
// Once per bridge lifetime for a given SOLIDWORKS process. A new pid
// means a genuinely new session, and re-arms it.
let clicked = CLICKED_FOR.lock().unwrap_or_else(|e| e.into_inner());
if *clicked == Some(pid) {
return None;
}
}
if !crate::caller::ab_available() {
return None;
}
let windows = crate::abwin::list_windows(Duration::from_secs(10)).ok()?;
for w in &windows {
let title = w.get("title").and_then(|t| t.as_str()).unwrap_or("");
let Some(hwnd) = w.get("hwnd").and_then(|h| h.as_i64()) else { continue };
if !is_crash_reporter_candidate(title) {
continue;
}
// ATTRIBUTION BEFORE ACTION. The crash reporter belongs to the crashed
// process (or is its child); a window we cannot attribute to the
// SOLIDWORKS we are launching is not ours to touch.
if w.get("pid").and_then(|p| p.as_i64()) != Some(pid as i64) {
continue;
}
let found = crate::caller::ad_callback(
"desktop_find_control",
json!({"hwnd": hwnd, "contains": CRASH_RESTART_BUTTON, "role": "button"}),
Duration::from_secs(20),
);
if !mentions_button(&found) {
continue;
}
let reason = format!(
"SOLIDWORKS crashed while the Adom bridge was starting it; clicking the crash \
reporter's \"{CRASH_RESTART_BUTTON}\" button once to finish the launch"
);
let clicked = crate::caller::ad_callback(
"desktop_ui_click",
json!({
"hwnd": hwnd,
"contains": CRASH_RESTART_BUTTON,
"role": "button",
"reason": reason,
"label": "Restarting SOLIDWORKS after a crash",
}),
Duration::from_secs(30),
);
update(|a| a.crash_recovery_clicked = true);
*CLICKED_FOR.lock().unwrap_or_else(|e| e.into_inner()) = Some(pid);
crate::ledger::record(
"solidworks_crash_reporter_restarted",
json!({
"hwnd": hwnd,
"windowTitle": title,
"solidworksPid": pid,
"button": CRASH_RESTART_BUTTON,
"reason": reason,
"ab": clicked.get("success").cloned().unwrap_or(Value::Null),
}),
);
return Some(clicked);
}
None
}
// ── hung / exhausted ────────────────────────────────────────────────────────
/// A memory figure worth MENTIONING. Above the highest reading ever measured on
/// a session that had genuinely run out (17.4 GB), so it cannot fire on a
/// working one — and it is ADVISORY, never a verdict.
pub const MEMORY_ADVISORY_MB: u64 = 17_800;
/// How long a busy-vs-dead sample must span before ~0 CPU means anything.
pub const DEAD_SAMPLE_SECS: u64 = 15;
/// BUSY, DEAD, OR UNKNOWN — and the difference is what a caller should tell the
/// user to do.
///
/// LIVE EVIDENCE, 2026-09-08: SOLIDWORKS at 17.9 GB with an unsaved assembly
/// went not-responding with a COM timeout for about five minutes. Windows said
/// "not responding", so `is_hung` said hung and the honest-sounding hint said
/// "only the user can end it". It was burning ~70% of a core, memory was still
/// creeping, there was NO dialog, and it RECOVERED once Drew saved. Telling
/// somebody to end a task that is about to finish is a worse answer than saying
/// "wait".
///
/// So a not-responding window is only DEAD when it has also done essentially no
/// work over a sample long enough to mean it (`DEAD_SAMPLE_SECS`) and has raised
/// no new window. Anything else with CPU moving is BUSY. Without two readings —
/// or with the sample too short — the answer is UNKNOWN, never "dead".
pub fn liveness_verdict(
responding: Option<bool>,
cpu_active_seconds: Option<f64>,
sample_seconds: Option<f64>,
memory_delta_mb: Option<i64>,
new_windows: Option<usize>,
) -> &'static str {
liveness_verdict_for(responding, cpu_active_seconds, sample_seconds, memory_delta_mb, new_windows, None)
}
/// Above this fraction of one core, a not-responding process is doing WORK.
/// 2% of a core over the sample: comfortably below the ~70% measured on the
/// session that recovered, and far above the idle jitter a truly wedged process
/// shows.
pub const BUSY_CPU_FRACTION: f64 = 0.02;
/// Memory moving at least this fast (MB per minute) is work too.
pub const BUSY_MEM_MB_PER_MIN: f64 = 8.0;
/// After this long not responding, "busy" stops being a useful answer even if a
/// trickle of work continues: nobody should be told to keep waiting forever.
pub const BUSY_PATIENCE_SECS: u64 = 15 * 60;
/// The verdict, with the total time spent not-responding taken into account.
///
/// B1: the first version compared ABSOLUTE figures — `cpu > 0.25s` or
/// `|Δmem| >= 8MB` — with no reference to the span they were measured over. A
/// third of a second of CPU across a ten-minute sample is a process doing
/// essentially nothing, and it read as `busy`, which tells the user to wait
/// forever. Work is a RATE.
pub fn liveness_verdict_for(
responding: Option<bool>,
cpu_active_seconds: Option<f64>,
sample_seconds: Option<f64>,
memory_delta_mb: Option<i64>,
new_windows: Option<usize>,
not_responding_for_secs: Option<u64>,
) -> &'static str {
match responding {
Some(true) => return "responding",
// We do not KNOW the window is unresponsive, so we cannot call the
// process busy or dead on the strength of its CPU alone.
None => return "unknown",
Some(false) => {}
}
// A RATE, not a total. Without a span there is no rate and no verdict.
let (Some(cpu), Some(span)) = (cpu_active_seconds, sample_seconds) else {
return "unknown";
};
if span <= 0.0 {
// Two readings in the same instant measure nothing.
return "unknown";
}
let cpu_fraction = cpu / span;
let mem_rate = memory_delta_mb.map(|d| (d.abs() as f64) * 60.0 / span).unwrap_or(0.0);
let working = cpu_fraction > BUSY_CPU_FRACTION || mem_rate >= BUSY_MEM_MB_PER_MIN;
if working {
// ...but not forever. A session that has been like this for a very long
// time is no longer something to keep waiting on, whatever the trickle.
if not_responding_for_secs.unwrap_or(0) >= BUSY_PATIENCE_SECS {
return "unknown";
}
return "busy";
}
// From here we are considering DEAD, which needs a sample worth the name.
if span < DEAD_SAMPLE_SECS as f64 {
return "unknown";
}
// B2: an UNREADABLE window census cannot support a dead verdict. A new
// window means something is being ASKED; not knowing whether there is one is
// not the same as knowing there is none.
match new_windows {
None => "unknown",
Some(n) if n > 0 => "unknown",
Some(_) => "dead",
}
}
/// What to TELL the user, per verdict. The busy case must not read as a
/// death sentence and the dead case must not read as "wait a bit".
pub fn verdict_advice(verdict: &str) -> &'static str {
match verdict {
"responding" => "SOLIDWORKS is answering; nothing is wrong with the message loop.",
"busy" => "SOLIDWORKS IS NOT RESPONDING BUT IT IS WORKING — CPU is being burned and/or memory is still moving, and no dialog is up. This is a long rebuild, a big save, or memory pressure, and it usually finishes: on 2026-09-08 a session in exactly this state recovered after about five minutes. TELL THE USER TO WAIT, and say what it was doing. Do NOT advise ending the task, and do not retry in a loop — every retry adds work to a machine that is already behind.",
"dead" => "SOLIDWORKS is not responding and is doing NO measurable work (no CPU over the sample, memory flat, no dialog). Nothing this bridge can send will get through. ONLY THE USER CAN END IT — tell them what it was doing when it stopped and let them decide.",
_ => "UNKNOWN: there were not two readings far enough apart to tell a thrashing session from a dead one. Poll swx_status again in 15-30 seconds rather than concluding anything — 'not responding' alone has already been observed on a session that recovered.",
}
}
/// Is SOLIDWORKS hung?
///
/// `None` = UNKNOWN, and unknown is the answer far more often than the first
/// version allowed. BOTH signals must be present and both must be knowable:
/// Windows says the window is not answering messages, AND a COM call this
/// bridge made EXCEEDED ITS BUDGET.
///
/// `com_timed_out` is `None` when there was no COM call to judge — in
/// particular when the com host is BUSY WITH OUR OWN LONG VERB. The first
/// version counted busy as a timeout, so a rebuild this bridge started made
/// both "independent" signals fire from one cause and reported the user's
/// working session as hung.
pub fn is_hung(responding: Option<bool>, com_timed_out: Option<bool>) -> Option<bool> {
match (responding, com_timed_out) {
(Some(r), Some(t)) => Some(!r && t),
_ => None,
}
}
/// Why SOLIDWORKS is out of room, if it PROVABLY is.
///
/// ONE signal is sufficient and it is not a threshold: **SOLIDWORKS accepted a
/// NewDocument against its own stock template and handed back nothing.** That is
/// what exhaustion looks like from the API, and it is what was actually observed.
///
/// Memory and document counts are NOT sufficient, in either direction. The
/// first version raised `host_app_exhausted` above 12 GB / 40 documents, and the
/// same machine the same week ran a healthy session at 13.1 GB with 33
/// documents open — so the verb told a working session to close its documents
/// and restart. Those figures are now advisory only ([`memory_advisory`]).
pub fn exhaustion(_private_mb: Option<u64>, _open_docs: Option<u64>, new_doc_empty: bool) -> Option<String> {
new_doc_empty.then(|| {
"SOLIDWORKS returned no document from its own stock template — the signature of a session \
that has run out of room"
.to_string()
})
}
/// The COMPOUND verdict, for a session that is not answering at all: it is
/// wedged AND a COM call timed out AND its memory is still climbing between two
/// reads. Any one of those alone is ordinary.
pub fn exhaustion_confirmed(
new_doc_empty: bool,
responding: Option<bool>,
com_timed_out: Option<bool>,
memory_rising: bool,
private_mb: Option<u64>,
) -> Option<String> {
if let Some(why) = exhaustion(private_mb, None, new_doc_empty) {
return Some(why);
}
if is_hung(responding, com_timed_out) == Some(true) && memory_rising {
return Some(format!(
"SOLIDWORKS is not responding, a COM call timed out, and its private memory is still \
climbing ({} MB)",
private_mb.map(|m| m.to_string()).unwrap_or_else(|| "unknown".into())
));
}
None
}
/// A figure worth putting in front of a human — and nothing more.
pub fn memory_advisory(private_mb: Option<u64>, working_set_mb: Option<u64>, open_docs: Option<u64>) -> Option<String> {
let mb = private_mb?;
if mb < MEMORY_ADVISORY_MB {
return None;
}
Some(format!(
"sldworks.exe is holding {mb} MB of private memory ({} MB resident{}). At 17.4 GB on \
2026-09-03 this session stopped being able to create documents. This is an OBSERVATION, \
not a verdict — SOLIDWORKS may be working perfectly.",
working_set_mb.map(|w| w.to_string()).unwrap_or_else(|| "unknown".into()),
open_docs.map(|d| format!(", {d} documents open")).unwrap_or_default(),
))
}
/// The hint that goes with `host_app_exhausted`.
pub const EXHAUSTED_HINT: &str =
"SOLIDWORKS is still running, but it has no room left: new documents come back empty and \
further work will fail in ways that look like unrelated bugs. Close the documents this \
bridge created (swx_reset_workspace) and try again. If that is not enough, ASK THE USER to \
restart SOLIDWORKS — it is their session and their unsaved work, and this bridge will not \
end it for them. Do NOT retry the same call in a loop; each attempt costs more memory.";
/// The hint that goes with `host_app_hung`.
pub const HUNG_HINT: &str =
"Windows reports the SOLIDWORKS window is not responding AND a COM call from this bridge \
exceeded its budget. Nothing this bridge can send will get through: the message loop is not \
running. ONLY THE USER CAN END IT — tell them SOLIDWORKS is not responding, say what it was \
doing when it stopped, and let them decide whether to wait or end the task. This bridge will \
not kill their CAD session to unblock itself; swx_kill exists but is gated behind their \
explicit opt-in for exactly this reason.";
// ── the report ──────────────────────────────────────────────────────────────
/// The `launch` block on `swx_status` and `swx_start`.
/// THE SESSION CAME UP AFTER WE GAVE UP — and it was still ours.
///
/// Live, 2026-09-11: the launch machine wrote the attempt off as `exhausted`
/// while the user was at the sign-in window, and SOLIDWORKS then started from a
/// surviving launcher window. `attributed` stayed FALSE on a session this bridge
/// really did launch, and everything that keys on attribution followed it down:
/// the quit gate's bridge-launched proof failed, and `swx_close_all_documents`
/// refused as though the session were the user's own.
///
/// The evidence that it is ours is already recorded and does not expire:
///
/// * some route of THIS attempt had a launcher log we could prove was ours
/// and it logged `Run Begin` — the chain really built something;
/// * the running `sldworks.exe` was created AT OR AFTER our invoke, so it
/// cannot be a session that predates us;
/// * and no other launcher chain was seen (`foreign_chain_seen`), so there is
/// no second candidate for whose launch this was.
///
/// All three, or nothing changes. Re-attribution on two of them would be the
/// "first CATSTART after our invoke" guess this bridge spent four adversarial
/// rounds removing.
/// Walk `sldworks.exe`'s parents. Returns HOW it is ours, or `None`.
///
/// Bounded, because a parent chain read from a live census can contain a cycle if
/// a pid was reused while we walked it.
fn descends_from_our_chain(census: &[ProcInfo], mut pid: u32, a: &Attempt) -> Option<String> {
let start = pid;
// ── A PID IS NOT AN IDENTITY, and Windows does not validate a parent id:
// `ppid` is recorded at creation and never cleared, so once our launcher
// exits (which it always does — the chain exits as soon as sldworks.exe is
// up) that NUMBER can belong to something else entirely. A bare
// `ours.contains(&parent)` would then accept a process whose parent merely
// REUSED the number of a launcher of ours that is long dead. Every other
// identity in this crate is (pid, creation time); so is this one.
let ours: Vec<ProcInfo> = our_identities(a);
if ours.is_empty() {
return reattr_refuse("no_identities");
}
let mut seen: Vec<u32> = Vec::new();
for _ in 0..16 {
if seen.contains(&pid) {
return reattr_refuse("parent_walk_cycle");
}
seen.push(pid);
let Some(p) = census.iter().find(|p| p.pid == pid) else {
return reattr_refuse("walk_pid_not_in_census");
};
let Some(parent) = p.ppid else { return reattr_refuse("no_parent_recorded") };
if let Some(mine) = ours.iter().find(|m| m.pid == parent) {
// THE PARENT IS USUALLY DEAD, and that is normal: the launcher chain
// exits the moment sldworks.exe is up. So it cannot be required to be
// alive — but if something IS alive on that pid, it must be the
// process we recorded, or the number has been reused and this is
// somebody else's tree.
if let Some(live) = census.iter().find(|c| c.pid == parent) {
if !mine.same_process(live) {
return reattr_refuse("alive_but_different_process");
}
}
let Some(child) = census.iter().find(|c| c.pid == start) else {
return reattr_refuse("child_not_in_census");
};
let Some(child_created) = child.created_at else {
return reattr_refuse("child_creation_time_unknown");
};
let Some(parent_created) = mine.created_at else {
return reattr_refuse("parent_creation_time_unknown");
};
// ── THE WINDOW. "A pid we once recorded" is not enough once that
// process is dead: the NUMBER is free, and a user's launcher that
// reuses it would have its SOLIDWORKS record our dead pid as its
// parent with nothing alive left to contradict it — and "the child
// is younger than the parent" is vacuously true for anything that
// started later. So the parent must have been SEEN ALIVE with this
// exact (pid, creation time) during the attempt, and the child must
// have been created between that creation time and shortly after we
// last watched it run. A process that appeared long after we last
// saw the parent alive is not its child.
let Some(last_seen) = a
.chain_last_seen
.iter()
.find(|(p, c, _)| *p == parent && *c == parent_created)
.map(|(_, _, t)| *t)
else {
return reattr_refuse("no_last_seen_record");
};
// CREATED WHILE THE PARENT RAN: at or after the parent's own
// creation, and no later than the last moment we saw the parent
// alive, plus one census interval for the gap between polls.
let grace = lineage_grace_secs();
if child_created < parent_created || child_created > last_seen + grace {
return reattr_refuse("child_outside_window");
}
return Some(format!(
"sldworks.exe descends from pid {parent} ({}, created {parent_created}), a process \
this attempt WATCHED RUN until {last_seen}, and was created at {child_created} — \
while that process was running (plus {grace}s for the census interval)",
mine.image
));
}
pid = parent;
}
reattr_refuse("parent_not_in_ours")
}
/// How long after we last SAW a parent alive a child may still be called its
/// descendant.
///
/// A REAL CHILD IS CREATED WHILE ITS PARENT RUNS. The only reason this is not
/// zero is that our knowledge of "while it ran" is a POLL: the parent can die in
/// the gap between two censuses, and a child created in that gap was still
/// created while it ran — we just did not look at the right moment. So the grace
/// is exactly the census interval, and nothing wider: a flat 30s was three
/// census intervals of slack invented to be safe, and slack in an identity rule
/// is the space a recycled pid lives in.
pub fn lineage_grace_secs() -> u64 {
poll_interval().as_secs().max(1)
}
/// The processes this attempt can prove it started, as IDENTITIES.
///
/// A recorded chain entry already carries its creation time. An `Attribution`
/// root may not (a `Start-Process -PassThru` pid arrives without one), and one
/// without a creation time cannot be matched by `same_process` — so it is only
/// usable if the census still holds a process of that pid whose creation time we
/// can read, which is exactly the "no handle, no kill" standard applied to a
/// question about lineage rather than about termination.
fn our_identities(a: &Attempt) -> Vec<ProcInfo> {
let mut out: Vec<ProcInfo> = Vec::new();
for p in a.chain.iter() {
if p.created_at.is_some() {
out.push(p.clone());
}
}
if let Some(r) = a.attribution.as_ref() {
if let Some(c) = r.created_at {
out.push(ProcInfo {
pid: r.pid,
image: "CATSTART.exe".into(),
created_at: Some(c),
ppid: None,
});
}
}
out
}
// NOTE, 0.6.3: there was a `log_authored_by_our_chain` here, offered as a second
// lineage proof. It is gone, and deliberately not replaced: a launcher log written
// by a thread of our chain proves OUR LAUNCHER RAN, which is a different claim
// from "the SOLIDWORKS now running came from it". A user starting SOLIDWORKS from
// the Start Menu while our chain was also running satisfied it, and the bridge
// would have adopted their session on that evidence. The parent walk is the only
// thing that answers the question actually being asked.
/// WHY RE-ATTRIBUTION DID NOT HAPPEN, recorded every time it does not.
///
/// This exists because the feature was inert for three releases and each time the
/// evidence for WHY was reconstructed by reading code and guessing. A refusal
/// that names its own clause turns that into one line of output. It is written to
/// the launch report (`reattributionRefusedBecause`) and to the ledger.
static REATTR_REASON: Mutex<Option<String>> = Mutex::new(None);
/// The pid of the SOLIDWORKS this bridge is ATTACHED to, as last reported by the
/// com host. 0 means unknown. Set by `swx_status`, which is the only place that
/// asks; read by the lineage walk so the process a caller MEANS is tested first.
static ATTACHED_SW_PID: AtomicU32 = AtomicU32::new(0);
pub fn note_attached_solidworks(pid: Option<u32>) {
ATTACHED_SW_PID.store(pid.unwrap_or(0), Ordering::SeqCst);
}
/// The SOLIDWORKS pid COM last reported being attached to (swx_status), if any.
pub fn attached_solidworks_pid() -> Option<u32> {
match ATTACHED_SW_PID.load(Ordering::SeqCst) {
0 => None,
p => Some(p),
}
}
fn reattr_refuse(reason: &str) -> Option<String> {
*REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()) = Some(reason.to_string());
None
}
fn reattr_clear() {
*REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
/// THE SOLIDWORKS THIS BRIDGE'S ATTRIBUTION IS ABOUT, if any.
///
/// A verb that ENDS a session asks this before it acts: an attribution record is
/// a claim about ONE process, and it may not authorise acting on another. `None`
/// means no claim has been made, which is not a licence either — it simply means
/// this check has nothing to say.
pub fn attributed_solidworks() -> Option<(u32, u64)> {
attempt().and_then(|a| a.attributed_sldworks)
}
pub fn reattribution_refused_because() -> Option<String> {
REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn reattribute_after_attach(os: &dyn LaunchOs, attached: bool) -> Option<String> {
// COM MUST ACTUALLY BE ATTACHED. The first version of this took "SOLIDWORKS
// is in the census" as its trigger and relabelled the attempt `attached`
// — which is a false claim about the only thing this bridge counts as
// success, and it broke the case the launch machine exists to report
// honestly: sldworks up, COM not answering, probably a sign-in. Being up is
// not being usable.
if !attached {
return reattr_refuse("not_attached");
}
let Some(a) = attempt() else { return reattr_refuse("no_attempt_recorded") };
if !a.finished || a.outcome.as_deref() == Some("attached") {
return reattr_refuse("outcome_not_eligible");
}
if a.foreign_chain_seen {
return reattr_refuse("foreign_chain_seen");
}
// A route of ours that reached Run Begin, WITH the root the invoke handed
// back — the same evidence standard as everywhere else in this module.
// THE ROUTE THAT PROVED ITSELF. Preferably the per-route row; failing that,
// the attempt's own `run_begin_seen`, which is the same first-hand fact
// recorded one level up (a route can end without a row — see `Stop`).
let proven_route: String = match a
.tries
.iter()
.find(|t| t.run_begin_seen && t.root_pid.is_some())
.or_else(|| a.tries.iter().find(|t| t.run_begin_seen))
{
Some(t) => t.route.to_string(),
None if a.run_begin_seen => a
.last_route_before_attach
.clone()
.or_else(|| a.tries.last().map(|t| t.route.to_string()))
.unwrap_or_else(|| "unknown".to_string()),
None => return reattr_refuse("route_unattributable"),
};
let Ok(census) = os.processes() else { return reattr_refuse("census_unreadable") };
// ── EVERY sldworks.exe, NOT "the first one in the census".
//
// `find` took whichever the census listed first, and if that process had no
// parent recorded the walk ended there — `no_parent_recorded` — while a
// second sldworks.exe sitting further down the list descended from our own
// launcher. That is the same defect `host_app_health` was rewritten for in
// 0.4.x ("EVERY sldworks.exe is reported, not just the first"), reappearing
// in the lineage walk.
//
// More than one is ordinary: a zombie from a crashed session, or the user's
// own session beside ours. The question is not "what is the SOLIDWORKS" but
// "does ANY running SOLIDWORKS descend from the chain we started", and the
// one we are ATTACHED to is asked first because it is the one a caller
// means.
let all_sw: Vec<&ProcInfo> = census.iter().filter(|p| p.is("SLDWORKS.exe")).collect();
if all_sw.is_empty() {
return reattr_refuse("no_sldworks_in_census");
}
// ── THE PROCESS THAT DESCENDS FROM OUR CHAIN MUST BE THE ONE COM IS
// ATTACHED TO. 0.6.6 used the attached pid only to ORDER the candidates
// and took the first that descended — so with three SOLIDWORKS on the
// machine (one COM-attached with no parent, one of ours, one of the
// user's) it proved lineage for OUR launcher's child and then labelled the
// session `attributed:true` while COM was attached to a different process
// entirely. Everything downstream reads that label: the quit gate's
// bridge-launched proof, and a quit ladder that builds its Identity from
// the COM pid. The bridge would have been willing to END a session nothing
// had proved was ours.
//
// Ordering is not a substitute for equality. The claim being made is "the
// session this bridge is talking to is the one it launched", and only the
// attached process can satisfy it.
let attached_pid = ATTACHED_SW_PID.load(Ordering::SeqCst);
let attached_pid = if attached_pid == 0 { None } else { Some(attached_pid) };
let Some(want) = attached_pid else {
return reattr_refuse("attached_pid_unknown");
};
// EVERY candidate is examined and every verdict recorded, so the ledger says
// what was considered rather than "the last one stands".
let mut per_candidate: Vec<Value> = Vec::new();
let mut proved: Option<(&ProcInfo, String)> = None;
for cand in &all_sw {
let why = descends_from_our_chain(&census, cand.pid, &a);
let clause = match &why {
Some(_) => "descendsFromOurChain".to_string(),
None => reattribution_refused_because().unwrap_or_else(|| "unknown".into()),
};
per_candidate.push(json!({
"pid": cand.pid,
"createdAt": cand.created_at,
"isAttachedProcess": cand.pid == want,
"verdict": clause,
}));
if why.is_some() && cand.pid == want && proved.is_none() {
proved = Some((cand, why.unwrap()));
}
}
let descends_but_not_attached: Vec<u32> = per_candidate
.iter()
.filter(|c| {
c.get("verdict").and_then(|v| v.as_str()) == Some("descendsFromOurChain")
&& c.get("isAttachedProcess").and_then(|v| v.as_bool()) == Some(false)
})
.filter_map(|c| c.get("pid").and_then(|v| v.as_u64()).map(|p| p as u32))
.collect();
let Some((sw, lineage)) = proved else {
crate::ledger::record(
"solidworks_launch_reattribution_refused",
json!({
"attachedPid": want,
"candidates": per_candidate,
"why": if descends_but_not_attached.is_empty() {
"no running SOLIDWORKS could be proved to descend from this attempt's chain"
} else {
"a SOLIDWORKS descending from our chain exists, but it is NOT the process COM \
is attached to — so nothing proves the ATTACHED session is ours, and it is \
the attached session every other verb acts on"
},
}),
);
return reattr_refuse(if descends_but_not_attached.is_empty() {
"no_candidate_descends"
} else {
"attached_pid_not_ours"
});
};
// The identity must be COMPLETE: a pid without a creation time is not one.
if sw.created_at.is_none() {
return reattr_refuse("sldworks_creation_time_unknown");
}
let Some(created) = sw.created_at else { return reattr_refuse("sldworks_creation_time_unknown") };
// ── LINEAGE, not just timing. "No foreign chain was seen" is the ABSENCE of
// an observation, and absence is not evidence — a launcher chain that
// started and finished between two censuses was never seen at all, and on
// that reading its SOLIDWORKS would have been adopted as ours purely for
// starting after our invoke. So there must be a POSITIVE link from this
// process back to something we know we started: a parent walk that reaches
// our own chain, or a launcher log authored by a thread of it.
//
// THE PARENT WALK IS THE ONLY LINEAGE PROOF. 0.6.1 also accepted "a
// launcher log written by a thread of our chain", and that was the wrong
// proposition: it proves OUR LAUNCHER RAN, which is not the same claim as
// "the SOLIDWORKS now running came from it". A user who starts SOLIDWORKS
// from the Start Menu while our chain is also running satisfies it, and
// the bridge would have adopted their session. Proving the wrong thing
// confidently is worse than proving nothing.
// NO SLACK. The bridge reads its own clock through the same call the
// creation times come from (GetSystemTimeAsFileTime), so "created at or
// after our invoke" needs none — see the Cargo.toml note on
// Win32_System_SystemInformation.
if created < a.invoked_at {
// It predates our invoke: somebody else's session, and ours never came.
return reattr_refuse("sldworks_predates_our_invoke");
}
let route = proven_route;
let (sw_pid, sw_created) = (sw.pid, created);
let root = a
.tries
.iter()
.find(|t| t.run_begin_seen && t.root_pid.is_some())
.and_then(|t| t.root_pid)
.or_else(|| a.attribution.as_ref().map(|r| r.pid));
mutate_store(|s| {
if let Some(at) = s.attempt.as_mut() {
at.outcome = Some("attachedAfterSignIn".into());
at.stage = Stage::Attached;
at.run_begin_seen = true;
// THE EXACT SESSION THIS CLAIM IS ABOUT. Everything downstream that
// acts on "the bridge launched this" — the quit gate's proof, and the
// ladder that builds its Identity from the COM pid — can now check
// that it is talking to the same process the claim was made about.
at.attributed_sldworks = Some((sw_pid, sw_created));
at.attributed_how = Some("reattributedAfterSignIn".into());
at.last_route_before_attach = Some(route.clone());
if at.attribution.is_none() {
if let (Some(pid), Some(c)) = (root, Some(created)) {
// The ROOT this route reported, with the creation time we
// have for it — not the SOLIDWORKS pid, which was never ours
// to claim as a launcher root.
let _ = c;
at.attribution = Some(Attribution {
pid,
created_at: None,
source: "reattributedAfterSignIn".to_string(),
});
}
}
}
});
reattr_clear();
crate::ledger::record(
"solidworks_launch_reattributed",
json!({
"route": route,
"sldworksPid": sw.pid,
"sldworksCreatedAt": created,
"attachedPid": want,
"isAttachedProcess": true,
"candidates": per_candidate,
"invokedAt": a.invoked_at,
"wasOutcome": a.outcome,
"lineage": lineage,
"why": "this attempt was written off while the user was at the 3DEXPERIENCE sign-in, \
and the session that came up afterwards is the one it started: our own chain \
logged Run Begin, sldworks.exe was created at or after our invoke, and no \
other launcher chain was seen",
}),
);
Some(format!(
"{lineage}; and that process (pid {sw_pid}, created {sw_created}) IS the one COM is \
attached to (pid {want}) — which is what makes this a claim about the session every other \
verb acts on, rather than about some other SOLIDWORKS that happens to be running"
))
}
/// WHICH SOLIDWORKS DID THIS LAUNCH START? Decided at attach (0.10.7).
///
/// MEASURED 2026-10-14 on the Connected edition: the launch recorded its root
/// (pid 53900, `startProcessPassThru`) and attached, but `attributedSolidworks`
/// stayed null — the sldworks.exe that came up (pid 70644, created 4 s after
/// the root) has ppid 40520, because the launcher chain RE-PARENTS. Only a
/// sign-in re-attribution ever set it, so a session swx_start had launched four
/// seconds earlier read `bridgeStarted:false`, and a long run could never
/// recycle it.
///
/// So, at attach: first ANCESTRY (a parent walk back to our own chain, the
/// strongest proof); failing that, THE LAUNCH WINDOW — the one sldworks.exe
/// that (a) was not in the baseline census taken before our invoke and (b) was
/// created between the attempt's start and the attach. NEVER when two could be
/// ours, never without a baseline, and never a process other than the one COM
/// is attached to (when that is known).
pub fn solidworks_attribution(
a: &Attempt,
census: &[ProcInfo],
attached_pid: Option<u32>,
ready_at: u64,
) -> Result<(u32, u64, &'static str), String> {
let sw: Vec<&ProcInfo> = census.iter().filter(|p| p.is("SLDWORKS.exe")).collect();
if sw.is_empty() {
return Err("no sldworks.exe in the census".into());
}
let must_be_attached = |p: &ProcInfo| -> Result<(), String> {
match attached_pid {
Some(want) if want != p.pid => Err(format!(
"the SOLIDWORKS this launch started (pid {}) is not the one COM is attached to (pid {want})",
p.pid
)),
_ => Ok(()),
}
};
// 1. ANCESTRY. The walk records its refusal reasons in the slot the sign-in
// re-attribution reports; this probe must not overwrite them.
let saved = reattribution_refused_because();
let by_lineage: Vec<&ProcInfo> =
sw.iter().copied().filter(|p| descends_from_our_chain(census, p.pid, a).is_some()).collect();
*REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()) = saved;
match by_lineage.as_slice() {
[one] => {
must_be_attached(one)?;
let created = one.created_at.ok_or("its creation time is unknown")?;
return Ok((one.pid, created, "ancestry"));
}
[] => {}
many => {
return Err(format!(
"{} SOLIDWORKS processes descend from this launch's chain — never attributed when more than one could be ours",
many.len()
))
}
}
// 2. THE LAUNCH WINDOW.
if !a.baseline_known {
return Err("the census before the invoke could not be taken, so no process can be called new".into());
}
let new: Vec<&ProcInfo> = sw
.iter()
.copied()
.filter(|p| !a.baseline_pids.contains(&p.pid))
.filter(|p| p.created_at.map(|c| c >= a.started_at && c <= ready_at).unwrap_or(false))
.collect();
match new.as_slice() {
[one] => {
must_be_attached(one)?;
Ok((one.pid, one.created_at.unwrap_or(0), "newProcessInLaunchWindow"))
}
[] => Err(format!(
"no sldworks.exe that was not already running was created between the launch's start ({}) and the attach ({ready_at})",
a.started_at
)),
many => Err(format!(
"{} sldworks.exe processes were created in the launch window — never attributed when more than one could be ours",
many.len()
)),
}
}
/// Apply [`solidworks_attribution`] to the current attempt — once, when it is
/// attached and makes no claim yet. Ledgered either way. Returns the claim.
pub fn attribute_on_attach(os: &dyn LaunchOs, attached_pid: Option<u32>) -> Option<(u32, u64)> {
let a = attempt()?;
if a.attributed_sldworks.is_some() {
return a.attributed_sldworks;
}
if !(a.stage == Stage::Attached || a.outcome.as_deref() == Some("attached")) {
return None;
}
let census = os.processes().ok()?;
// THE WINDOW CLOSES AT THE ATTACH, not now: a SOLIDWORKS the user starts
// tomorrow is "new" to a record from today, and must never inherit it.
let ready_at = a.stage_at;
match solidworks_attribution(&a, &census, attached_pid, ready_at) {
Ok((pid, created, how)) => {
mutate_store(|s| {
if let Some(at) = s.attempt.as_mut() {
if at.id == a.id && at.attributed_sldworks.is_none() {
at.attributed_sldworks = Some((pid, created));
at.attributed_how = Some(how.into());
}
}
});
crate::ledger::record(
"solidworks_attributed",
json!({"pid": pid, "createdAt": created, "how": how, "attachedPid": attached_pid,
"launchStartedAt": a.started_at, "readyAt": ready_at,
"launchRoot": a.attribution.as_ref().map(|r| r.pid)}),
);
Some((pid, created))
}
Err(why) => {
static LAST: Mutex<Option<u64>> = Mutex::new(None);
let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
if *last != Some(a.id) {
*last = Some(a.id);
crate::ledger::record("solidworks_attribution_refused", json!({"why": why, "attachedPid": attached_pid}));
}
None
}
}
}
pub fn report(os: &dyn LaunchOs) -> Value {
report_with_attach(os, false)
}
/// HAS THIS RECORD OUTLIVED EVERY PROCESS IT NAMES? MEASURED 2026-10-12 (hosted
/// 0.9.27): with SOLIDWORKS not running, `launch` still said stage "attached",
/// advice "SOLIDWORKS is up and COM-attached. Carry on.", about a launch of
/// 2026-10-02 — ten days and several bridge restarts after its process was
/// gone. The record names processes by IDENTITY (pid + creation time); when the
/// census holds none of them, the launch is over. `None` = something it names is
/// still alive, or it names nothing (cannot tell).
pub fn record_outlived(a: &Attempt, census: &[ProcInfo]) -> Option<String> {
let mut named: Vec<(u32, Option<u64>, String)> = Vec::new();
if let Some(at) = &a.attribution {
named.push((at.pid, at.created_at, "the launch's attributed process".into()));
}
if let Some((pid, created)) = a.attributed_sldworks {
named.push((pid, Some(created), "the SOLIDWORKS it attached to".into()));
}
for p in &a.chain {
named.push((p.pid, p.created_at, p.image.clone()));
}
if named.is_empty() {
return None;
}
let alive = |pid: u32, created: Option<u64>| {
census.iter().any(|p| {
p.pid == pid
&& match (created, p.created_at) {
(Some(a), Some(b)) => a.abs_diff(b) <= 2,
_ => true,
}
})
};
if named.iter().any(|(pid, c, _)| alive(*pid, *c)) {
return None;
}
Some(format!(
"none of the processes this record names is running any more ({}); the launch it describes is over",
named.iter().map(|(pid, _, what)| format!("pid {pid}: {what}")).collect::<Vec<_>>().join(", ")
))
}
/// The same report, told whether COM is attached RIGHT NOW — which is what lets
/// a session that came up after a sign-in be re-claimed (see
/// [`reattribute_after_attach`]). Only `swx_status` knows that fact, so only
/// `swx_status` passes it.
pub fn report_with_attach(os: &dyn LaunchOs, attached: bool) -> Value {
// THE SESSION MAY HAVE COME UP AFTER WE GAVE UP. Asked here because this is
// the function every caller reads attribution from — and the live failure was
// that `attributed:false` outlived the launch that earned it. Cheap: it does
// nothing at all unless the attempt is finished, un-attached, and SOLIDWORKS
// is now running.
let evidence = reattribute_after_attach(os, attached);
let refused = reattribution_refused_because();
// RECONCILED ON ATTACH (0.9.13). MEASURED 2026-10-02: after Drew signed in,
// the record stayed {stage:"helperSignIn", outcome:"abandoned"} while
// SOLIDWORKS was running and ready (the bridge had been respawned mid-launch,
// so no supervisor saw the attach). When COM is attached and the record still
// names a pre-attach stage, the STAGE moves to `attached`; the outcome and
// the attribution keep their evidence, and the reason re-attribution gave is
// kept as `supersededByAttach`.
if attached && evidence.is_none() {
if let Some(a) = attempt() {
if a.finished && a.stage != Stage::Attached && a.outcome.as_deref() != Some("attached") {
let why = refused.clone().unwrap_or_else(|| "not re-attributed".into());
mutate_store(|s| {
if let Some(at) = s.attempt.as_mut() {
at.stage = Stage::Attached;
at.superseded_by_attach = Some(why);
at.sign_in_window = None;
}
});
}
}
}
let store = read_store(|s| s.clone());
let live = in_flight(os);
let backoff = backoff_remaining();
match store.attempt {
None => json!({
"inFlight": false,
"preferredRoute": store.preferred_route,
"routesAvailable": ROUTES,
"crashRecovery": if crash_recovery_enabled() { "armed" } else { "off" },
"_hint": "No launch has been attempted by this bridge. swx_start begins one.",
}),
Some(a) => {
let mut v = a.to_json();
if let Some(obj) = v.as_object_mut() {
// WHY THE SESSION WAS OR WAS NOT RE-CLAIMED, every time. This
// feature was inert for three releases and each diagnosis meant
// reading code and guessing; a named clause makes it one line of
// output.
obj.insert("reattributionEvidence".into(), json!(evidence));
obj.insert("reattributionRefusedBecause".into(), json!(refused));
obj.insert("inFlight".into(), json!(live.is_some()));
obj.insert("preferredRoute".into(), json!(store.preferred_route));
obj.insert("routesAvailable".into(), json!(ROUTES));
obj.insert("advice".into(), json!(a.stage.advice()));
obj.insert("consecutiveFailures".into(), json!(store.consecutive_failures));
obj.insert("backoffSeconds".into(), json!(backoff));
// A RECORD THAT OUTLIVED ITS PROCESSES IS ENDED (0.9.28): never
// "attached", never "carry on" while nothing it names runs.
if !attached && live.is_none() && (a.finished || a.stage == Stage::Attached) {
if let Ok(census) = os.processes() {
if let Some(why) = record_outlived(&a, &census) {
obj.insert("recordedStage".into(), json!(a.stage.name()));
obj.insert("stage".into(), json!("ended"));
obj.insert("finished".into(), json!(true));
obj.insert("endedWhy".into(), json!(why));
obj.insert(
"advice".into(),
json!("This launch record is OVER: the processes it names are gone, so SOLIDWORKS from \
that launch is not running. Read `solidworks.running` for now; swx_start begins \
a new launch."),
);
}
}
}
}
v
}
}
}
// ── the host-app health block ───────────────────────────────────────────────
/// Remembered private-memory readings, so "still climbing" is a MEASUREMENT
/// rather than a guess: (pid, previous, latest).
#[derive(Clone, Copy)]
struct Sample {
pid: u32,
mb: Option<u64>,
cpu: Option<f64>,
/// When this reading was taken, at MILLISECOND resolution. Whole seconds made
/// the sample span collapse to 0 whenever two reads landed inside one second
/// — and a zero span is "unknown", so a caller who asked twice quickly was
/// told the instrument had nothing, on a machine that was plainly working.
at_ms: u128,
/// When this process was FIRST seen not responding in an unbroken run. A
/// "busy" answer is only useful while it is still plausible, so the ladder of
/// advice needs to know how long the user has already been waiting.
not_responding_since: Option<u64>,
}
static MEM_SAMPLES: Mutex<Option<Sample>> = Mutex::new(None);
/// What the PREVIOUS reading of this process said, and how long ago.
///
/// Two readings are the whole instrument: a single "not responding" has already
/// been observed on a session that recovered, so a verdict from one reading is a
/// guess. Returns (rising, cpu delta, sample span seconds, memory delta MB).
#[allow(clippy::type_complexity)]
fn sample_process(
pid: u32,
mb: Option<u64>,
cpu: Option<f64>,
responding: Option<bool>,
) -> (bool, Option<f64>, Option<f64>, Option<i64>, Option<u64>) {
let now = epoch_secs();
let mut g = MEM_SAMPLES.lock().unwrap_or_else(|e| e.into_inner());
let prev = match *g {
Some(p) if p.pid == pid => Some(p),
_ => None,
};
// An unbroken run of not-responding readings; any responding reading resets it.
let since = match (responding, prev.and_then(|p| p.not_responding_since)) {
(Some(false), Some(t)) => Some(t),
(Some(false), None) => Some(now),
_ => None,
};
let now_ms = crate::launchos::epoch_ms();
*g = Some(Sample { pid, mb, cpu, at_ms: now_ms, not_responding_since: since });
let not_responding_for = since.map(|t| now.saturating_sub(t));
let Some(p) = prev else { return (false, None, None, None, not_responding_for) };
let span = Some(now_ms.saturating_sub(p.at_ms) as f64 / 1000.0);
let cpu_delta = match (cpu, p.cpu) {
(Some(a), Some(b)) => Some((a - b).max(0.0)),
_ => None,
};
let mem_delta = match (mb, p.mb) {
(Some(a), Some(b)) => Some(a as i64 - b as i64),
_ => None,
};
let rising = mem_delta.map(|d| d > 0).unwrap_or(false);
(rising, cpu_delta, span, mem_delta, not_responding_for)
}
/// What SOLIDWORKS is doing, read WITHOUT COM — the three questions COM cannot
/// answer when the answer is "it is wedged".
///
/// EVERY sldworks.exe is reported, not just the first. The first version took
/// whichever the census listed first, so a zombie left by a crashed session
/// supplied the memory, the handles and the `hung` verdict for a caller asking
/// about the session the user is working in.
pub fn host_app_health(
os: &dyn LaunchOs,
open_docs: Option<u64>,
com_timed_out: Option<bool>,
com_pid: Option<u32>,
) -> Value {
let census = os.processes();
let all: Vec<ProcInfo> = census
.as_ref()
.ok()
.map(|c| c.iter().filter(|p| p.is("SLDWORKS.exe")).cloned().collect())
.unwrap_or_default();
if all.is_empty() {
return json!({
"processId": Value::Null,
"running": match census { Ok(_) => json!(false), Err(_) => Value::Null },
"responding": Value::Null,
"hung": Value::Null,
"processes": [],
"processCount": match census { Ok(_) => json!(0), Err(_) => Value::Null },
"censusError": census.err(),
});
}
// WHICH ONE are the numbers about? The process COM is attached to, when COM
// can say; otherwise the newest, which is the one a user just started — and
// the answer says which rule was used.
let (chosen, basis) = match com_pid.and_then(|p| all.iter().find(|x| x.pid == p)) {
Some(p) => (p.clone(), "the process COM is attached to"),
None => {
let newest = all
.iter()
.max_by_key(|p| p.created_at.unwrap_or(0))
.cloned()
.expect("non-empty");
(newest, "the newest sldworks.exe (COM could not say which it is attached to)")
}
};
let hwnd = os.main_window_of_pid(chosen.pid);
let responding = hwnd.and_then(|h| os.window_responding(h));
let metrics = os.process_metrics(chosen.pid);
let (rising, cpu_delta, sample_span, mem_delta, not_responding_for) =
sample_process(chosen.pid, metrics.private_mb, metrics.cpu_seconds, responding);
// `None` when the census could not be read — which must NOT support a dead
// verdict (B2).
let new_windows = os.windows_of_pid(chosen.pid).ok().map(|w| {
w.iter().filter(|x| x.is_dialog() && x.visible).count()
});
let verdict = liveness_verdict_for(
responding,
cpu_delta,
sample_span,
mem_delta,
new_windows,
not_responding_for,
);
let hung = is_hung(responding, com_timed_out);
let confirmed = exhaustion_confirmed(false, responding, com_timed_out, rising, metrics.private_mb);
json!({
"processId": chosen.pid,
"running": true,
"startedAt": chosen.created_at,
"mainHwnd": hwnd,
"responding": responding,
"privateMemoryMB": metrics.private_mb,
"workingSetMB": metrics.working_set_mb,
"memoryRising": rising,
"handles": metrics.handles,
"openDocs": open_docs,
"hung": hung,
// BUSY vs DEAD. `hung` answers "is the message loop running"; this
// answers "is anything actually happening", which is the question a
// caller needs to decide between "wait" and "only the user can end it".
"verdict": verdict,
"verdictAdvice": verdict_advice(verdict),
"cpuActiveSeconds": cpu_delta,
"cpuSampleSeconds": sample_span,
// The RATE the verdict was decided on, and how long the user has been
// waiting — so "busy" can be reported as "like this for N minutes".
"cpuFractionOfCore": match (cpu_delta, sample_span) {
(Some(c), Some(s)) if s > 0.0 => json!((c / s * 1000.0).round() / 1000.0),
_ => Value::Null,
},
"notRespondingForSeconds": not_responding_for,
"memoryDeltaMB": mem_delta,
"visibleDialogs": new_windows,
"hungNote": if hung.is_none() {
json!("UNKNOWN: 'hung' needs BOTH a not-responding window AND a COM call that \
exceeded its budget. One of those could not be read — in particular, a com \
host busy with this bridge's own long verb is not evidence of anything.")
} else { Value::Null },
"exhausted": confirmed,
"memoryAdvisory": memory_advisory(metrics.private_mb, metrics.working_set_mb, open_docs),
"processCount": all.len(),
"processes": all.iter().map(|p| p.to_json()).collect::<Vec<_>>(),
"reportedProcessBasis": basis,
})
}
/// Is SOLIDWORKS running at all, as far as the OS layer can tell?
/// `None` means the census failed — never read that as "no".
pub fn sldworks_running(os: &dyn LaunchOs) -> Option<bool> {
os.processes().ok().map(|c| c.iter().any(|p| p.is("SLDWORKS.exe")))
}
#[cfg(test)]
mod tests {
use super::*;
fn p(pid: u32, image: &str, created: Option<u64>, ppid: Option<u32>) -> ProcInfo {
ProcInfo { pid, image: image.to_string(), created_at: created, ppid }
}
/// An attempt that watched one launcher run, for the lineage tests.
fn watched(launcher_pid: u32, created: u64, last_seen: u64) -> Attempt {
let mut a = unfinished_placeholder("test.lnk");
a.chain = vec![p(launcher_pid, "SWXDesktopLauncher.exe", Some(created), None)];
a.chain_last_seen = vec![(launcher_pid, created, last_seen)];
a
}
/// PID REUSE, which is the whole reason lineage is not "the pid matches".
///
/// Our launcher dies. The user's launcher is issued the SAME NUMBER. Their
/// SOLIDWORKS records that number as its parent, and every cheap test passes:
/// the pid matches one we recorded, the child is younger than our dead
/// parent, and nothing is alive to contradict it. Only "created while the
/// parent we WATCHED was still running" refuses it.
#[test]
fn a_child_created_after_we_last_saw_the_parent_alive_is_not_its_descendant() {
let created = 1_000_000u64;
let last_seen = 1_000_010u64;
let a = watched(4242, created, last_seen);
let grace = lineage_grace_secs();
// WHILE WE WATCHED IT RUN: this is a child.
let ok = vec![
p(9001, "SLDWORKS.exe", Some(last_seen), Some(4242)),
p(4242, "SWXDesktopLauncher.exe", Some(created), None),
];
assert!(descends_from_our_chain(&ok, 9001, &a).is_some(), "created while the parent ran");
// The parent is DEAD and the child appeared inside the census gap: still
// a child, because our knowledge of "while it ran" is a poll.
let gap = vec![p(9001, "SLDWORKS.exe", Some(last_seen + grace), Some(4242))];
assert!(
descends_from_our_chain(&gap, 9001, &a).is_some(),
"one census interval after we last looked is still inside its lifetime as far as we know"
);
// LONG AFTER: the number was recycled, and this is somebody else's.
let reused = vec![p(9001, "SLDWORKS.exe", Some(last_seen + grace + 60), Some(4242))];
assert!(
descends_from_our_chain(&reused, 9001, &a).is_none(),
"a process created long after we last saw that pid alive is not its child — the NUMBER \
was reused, and nothing alive is left to say so"
);
// OLDER THAN THE PARENT: impossible either way.
let older = vec![p(9001, "SLDWORKS.exe", Some(created - 1), Some(4242))];
assert!(descends_from_our_chain(&older, 9001, &a).is_none(), "a child cannot predate its parent");
// ALIVE ON THAT PID, but a DIFFERENT process: the number is in use by
// someone else right now, so the walk refuses even inside the window.
let alive_other = vec![
p(9001, "SLDWORKS.exe", Some(last_seen), Some(4242)),
p(4242, "SWXDesktopLauncher.exe", Some(created + 5), None),
];
assert!(
descends_from_our_chain(&alive_other, 9001, &a).is_none(),
"something else holds that pid now: not our process, not our child"
);
// NEVER SEEN ALIVE: a pid in `chain` with no `chain_last_seen` row is a
// pid we recorded and never watched, which cannot bound anything.
let mut unseen = watched(4242, created, last_seen);
unseen.chain_last_seen.clear();
assert!(
descends_from_our_chain(&ok, 9001, &unseen).is_none(),
"a parent we never watched run cannot date its children"
);
}
/// RE-ATTRIBUTION IS ONLY POSSIBLE FOR AN ATTRIBUTED ROUTE — a RULE, not an
/// accident of how `our_identities` happens to be built.
///
/// `explorer` and `cmdStart` cannot say what they started, so nothing of
/// theirs is ever recorded as our chain and there is no identity to walk a
/// parent back to. A session that came up after one of them may well BE ours,
/// and this bridge still will not claim it, because it cannot prove it.
#[test]
fn an_unattributable_route_has_no_identities_to_walk_back_to() {
let mut a = unfinished_placeholder("test.lnk");
// An unattributable route: no attribution root, and nothing recorded as
// our chain because `attributed_chain` returns None without one.
a.attribution = None;
a.chain = Vec::new();
assert!(
our_identities(&a).is_empty(),
"with no attributed root there is no identity to prove lineage against"
);
let census = vec![p(9001, "SLDWORKS.exe", Some(2_000_000), Some(4242))];
assert!(
descends_from_our_chain(&census, 9001, &a).is_none(),
"so no session can descend from it, however plausible the timing"
);
// ...and a root WITHOUT a creation time is equally unusable: a pid that
// arrived without one cannot be matched by `same_process`.
a.attribution = Some(Attribution { pid: 4242, created_at: None, source: "test".into() });
assert!(
our_identities(&a).is_empty(),
"a root with no creation time is not an identity — same rule as 'no handle, no kill'"
);
}
#[test]
fn the_allowlist_never_reaches_a_daemon_or_anything_older_than_our_invoke() {
let invoked = 1_000_000;
assert!(may_terminate("CATSTART.exe", Some(invoked), invoked), "created AT our invoke");
assert!(may_terminate("catstart.exe", Some(invoked + 1), invoked), "image match is case-insensitive");
// NO BACKWARD SLACK. One second is exactly how long it takes to click a
// Start Menu, and the 0.4.0 version adopted anything within it.
assert!(!may_terminate("CATSTART.exe", Some(invoked - 1), invoked), "one second older is THEIRS");
assert!(!may_terminate("CATSTART.exe", None, invoked), "unknown age is never ours");
for daemon in [
"3DEXPERIENCELauncher.exe",
"3DEXPERIENCELauncherSysTray.exe",
"3DEXPERIENCELauncherService.exe",
"3DEXPERIENCELauncherAnythingElse.exe",
] {
assert!(!may_terminate(daemon, Some(invoked + 1), invoked), "{daemon} is never terminable");
}
for never in ["SLDWORKS.exe", "sldworks.exe", "explorer.exe", "swx-bridge.exe", "notepad.exe"] {
assert!(!may_terminate(never, Some(invoked + 1), invoked), "{never} is never terminable");
}
}
#[test]
fn nothing_is_adopted_without_a_root_the_invoke_handed_back() {
let invoked = 1_000_000;
// Two indistinguishable chains, both younger than our invoke, neither
// in the baseline. Same parent — which is what route `explorer`
// produces for OUR launch too, so parentage cannot break the tie.
let census = vec![
p(90010, "CATSTART.exe", Some(invoked + 1), Some(4444)),
p(90011, "SWXDesktopLauncher.exe", Some(invoked + 2), Some(90010)),
p(70003, "CATSTART.exe", Some(invoked + 2), Some(4444)),
p(70004, "SWXDesktopLauncher.exe", Some(invoked + 3), Some(70003)),
p(40, "SLDWORKS.exe", Some(invoked + 5), Some(90011)),
];
// NO ROOT: nothing is ours, however obvious it looks.
assert!(
attributed_chain(&census, None, &[], &[], invoked).is_none(),
"without an attribution the answer is 'I cannot tell', not a guess"
);
assert!(
chain_candidates(&census, &[], &[], invoked).is_empty(),
"the descendant walk cannot invent a root"
);
// WITH the root the invoke handed back, the chain is exact.
let root = Attribution {
pid: 90010,
created_at: Some(invoked + 1),
source: "shellExecuteHandle".into(),
};
let mine = attributed_chain(&census, Some(&root), &[], &[], invoked).unwrap();
let pids: Vec<u32> = mine.iter().map(|x| x.pid).collect();
assert_eq!(pids, vec![90010, 90011], "the root and its descendants only: {mine:?}");
assert!(!pids.contains(&40), "sldworks.exe descends from our chain and is still never terminable");
assert!(!pids.contains(&70003) && !pids.contains(&70004), "the user's chain is untouched");
// A root whose creation time we could not read is not an identity at
// all — the answer is "no attribution", not "an empty chain".
let bare = Attribution { pid: 90010, created_at: None, source: "x".into() };
assert!(
attributed_chain(&census, Some(&bare), &[], &[], invoked).is_none(),
"this bridge does not terminate a bare number"
);
// A root whose number the MACHINE says now belongs to something else:
// the lineage is void, so its apparent children are not adopted either.
let stale = Attribution { pid: 90010, created_at: Some(invoked - 500), source: "x".into() };
assert!(
attributed_chain(&census, Some(&stale), &[], &[], invoked).unwrap().is_empty(),
"a contradicted root seeds nothing"
);
// ...but a root the census CANNOT SEE is not contradicted, and still
// carries its children. `ShellExecuteExW` hands back whatever the shell
// created, which may not be one of the two terminable launcher images.
let unwatched = Attribution { pid: 500, created_at: Some(invoked + 1), source: "shellExecuteHandle".into() };
let via_shim = vec![
p(501, "CATSTART.exe", Some(invoked + 2), Some(500)),
p(502, "SWXDesktopLauncher.exe", Some(invoked + 3), Some(501)),
];
let found = attributed_chain(&via_shim, Some(&unwatched), &[], &[], invoked).unwrap();
let pids: Vec<u32> = found.iter().map(|x| x.pid).collect();
assert_eq!(pids, vec![501, 502], "lineage crosses a root we may not terminate: {found:?}");
}
#[test]
fn a_recorded_member_is_re_confirmed_by_identity_not_by_its_number() {
let invoked = 1_000_000;
let recorded = vec![p(90011, "SWXDesktopLauncher.exe", Some(invoked + 2), Some(90010))];
let root = Attribution { pid: 90010, created_at: Some(invoked + 1), source: "x".into() };
// CATSTART has exited (it does, ~3s into a healthy launch) and its
// number now belongs to a chain the user started two minutes later.
let census = vec![
p(90010, "CATSTART.exe", Some(invoked + 120), Some(4444)),
p(90011, "SWXDesktopLauncher.exe", Some(invoked + 2), Some(90010)),
];
let mine = attributed_chain(&census, Some(&root), &recorded, &[], invoked).unwrap();
let pids: Vec<u32> = mine.iter().map(|x| x.pid).collect();
assert_eq!(pids, vec![90011], "the recycled root is not re-adopted: {mine:?}");
}
#[test]
fn a_process_is_only_the_same_process_when_its_start_time_agrees() {
let a = p(10, "CATSTART.exe", Some(1000), None);
assert!(a.same_process(&p(10, "CATSTART.exe", Some(1000), None)));
assert!(!a.same_process(&p(10, "CATSTART.exe", Some(1001), None)), "a reused pid");
assert!(!a.same_process(&p(10, "SWXDesktopLauncher.exe", Some(1000), None)));
assert!(
!p(10, "CATSTART.exe", None, None).same_process(&p(10, "CATSTART.exe", None, None)),
"two unknowns are not a match"
);
}
#[test]
fn the_route_plan_starts_with_what_worked_and_still_tries_everything() {
assert_eq!(route_plan(None), ROUTES.to_vec());
let plan = route_plan(Some("cmdStart"));
assert_eq!(plan[0], "cmdStart");
assert_eq!(plan.len(), ROUTES.len(), "no route is dropped, and none is repeated");
assert_eq!(route_plan(Some("rm -rf /")), ROUTES.to_vec(), "a bogus route is ignored, not run");
assert!(!is_route("rm -rf /"));
}
#[test]
fn a_thrashing_solidworks_is_busy_not_dead() {
// LIVE, 2026-09-08: 17.9 GB, unsaved assembly, not-responding + COM
// timeout for ~5 minutes — and burning ~70% of a core with memory
// creeping and no dialog. It RECOVERED. "Only the user can end it" was
// the wrong advice, and telling somebody to end a task that is about to
// finish is worse than telling them to wait.
assert_eq!(
liveness_verdict(Some(false), Some(21.0), Some(30.0), Some(40), Some(0)),
"busy"
);
// Memory moving on its own is work too.
assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(30.0), Some(60), Some(0)), "busy");
// BUSY is a RATE, so a SHORT sample is fine — but a ZERO-length one
// measures nothing at all and is UNKNOWN, not busy.
assert_eq!(liveness_verdict(Some(false), Some(5.0), Some(2.0), Some(0), Some(0)), "busy");
assert_eq!(liveness_verdict(Some(false), Some(12.0), Some(0.0), Some(0), Some(0)), "unknown");
// A TRICKLE over a long sample is NOT work: 0.3s of CPU across ten
// minutes is a process doing essentially nothing, and calling that busy
// tells the user to wait for ever.
assert_eq!(liveness_verdict(Some(false), Some(0.3), Some(600.0), Some(0), Some(0)), "dead");
// ...and an unreadable window census cannot support a dead verdict.
assert_eq!(liveness_verdict(Some(false), Some(0.3), Some(600.0), Some(0), None), "unknown");
// Real work that has been going on far too long stops being a reason to
// keep waiting.
assert_eq!(
liveness_verdict_for(Some(false), Some(300.0), Some(600.0), Some(0), Some(0), Some(20 * 60)),
"unknown"
);
// NO work over a long enough sample, and nothing on screen: dead.
assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(30.0), Some(0), Some(0)), "dead");
// A dialog means something is being ASKED, not that nothing is happening.
assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(30.0), Some(0), Some(1)), "unknown");
// ONE reading, or a sample too short to mean anything, is UNKNOWN —
// never "dead". This is the guard that would have stopped the wrong call.
assert_eq!(liveness_verdict(Some(false), None, None, None, None), "unknown");
assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(3.0), Some(0), Some(0)), "unknown");
assert!(DEAD_SAMPLE_SECS >= 15, "a dead verdict needs a sample worth the name");
// A responding app is not a liveness question at all.
assert_eq!(liveness_verdict(Some(true), None, None, None, None), "responding");
assert_eq!(liveness_verdict(None, Some(0.0), Some(30.0), Some(0), Some(0)), "unknown");
}
#[test]
fn the_verdict_advice_says_wait_for_busy_and_never_for_dead() {
let busy = verdict_advice("busy").to_lowercase();
assert!(busy.contains("wait"), "busy must advise waiting: {busy}");
assert!(!busy.contains("only the user can end"), "busy must not read as a death sentence");
assert!(busy.contains("do not advise ending"));
let dead = verdict_advice("dead").to_lowercase();
assert!(dead.contains("only the user can end it"));
assert!(!dead.contains("tell the user to wait"));
let unknown = verdict_advice("unknown").to_lowercase();
assert!(unknown.contains("poll") && unknown.contains("again"));
}
#[test]
fn hung_needs_two_independent_signals_and_answers_unknown_otherwise() {
assert_eq!(is_hung(Some(false), Some(true)), Some(true));
assert_eq!(is_hung(Some(false), Some(false)), Some(false), "a rebuild is not a hang");
assert_eq!(is_hung(Some(true), Some(true)), Some(false), "a COM timeout alone is a modal");
// UNKNOWN, not false. `None` is what a com host BUSY WITH OUR OWN LONG
// VERB produces — the 0.4.0 code counted that as a timeout, so the
// bridge reported a working session as hung whenever it was itself
// doing something slow.
assert_eq!(is_hung(Some(false), None), None);
assert_eq!(is_hung(None, Some(true)), None);
}
#[test]
fn exhaustion_is_claimed_only_from_the_one_signal_that_proves_it() {
// THE measured healthy session on the same machine the same week.
assert!(exhaustion(Some(13_100), Some(33), false).is_none());
assert!(exhaustion(Some(17_465), Some(52), false).is_none(), "even the wedged session's numbers");
assert!(exhaustion(None, None, false).is_none());
// SOLIDWORKS accepting a NewDocument and handing back nothing is the
// signature, and it is sufficient on its own.
assert!(exhaustion(None, None, true).unwrap().contains("stock template"));
// The compound verdict needs all three, and is UNKNOWN-safe.
assert!(exhaustion_confirmed(false, Some(false), Some(true), true, Some(20_000)).is_some());
assert!(exhaustion_confirmed(false, Some(false), Some(true), false, Some(20_000)).is_none(), "not rising");
assert!(exhaustion_confirmed(false, Some(false), None, true, Some(20_000)).is_none(), "no COM verdict");
assert!(exhaustion_confirmed(false, Some(true), Some(true), true, Some(20_000)).is_none(), "responding");
}
#[test]
fn the_memory_figure_is_advisory_and_only_above_what_was_ever_measured() {
assert!(memory_advisory(Some(13_100), Some(9_000), Some(33)).is_none(), "a healthy big session");
assert!(memory_advisory(Some(17_465), Some(12_000), Some(52)).is_none(), "even AT the measurement");
let hint = memory_advisory(Some(18_000), Some(12_000), Some(52)).unwrap();
assert!(hint.contains("18000") && hint.contains("12000"), "both figures, side by side: {hint}");
assert!(hint.contains("not a verdict"), "{hint}");
assert!(memory_advisory(None, None, None).is_none(), "no reading is not an observation");
}
#[test]
fn a_control_lookup_is_a_find_only_when_it_says_so_structurally() {
// The QUERY ECHO: the bridge sent `contains: "Restart SOLIDWORKS"`, so
// any reply quoting it back contains the button name. Zero controls.
assert!(!mentions_button(&json!({
"success": true,
"query": {"hwnd": 111, "contains": "Restart SOLIDWORKS", "role": "button"},
"controls": [], "count": 0
})));
assert!(!mentions_button(&json!({"success": false})));
assert!(!mentions_button(&json!({"success": true, "found": false, "name": "Restart SOLIDWORKS Design"})));
assert!(!mentions_button(&json!({"success": true, "controls": [{"name": "Close"}]})));
assert!(!mentions_button(&json!({"success": true, "somethingUnrecognised": 1})), "an unknown shape is a NO");
// ...and the shapes that ARE a find.
assert!(mentions_button(&json!({
"success": true, "controls": [{"name": "Restart SOLIDWORKS Design", "invokable": true}]
})));
assert!(mentions_button(&json!({"success": true, "found": true, "name": "Restart SOLIDWORKS Design"})));
assert!(mentions_button(&json!({
"success": true, "control": {"automationId": "btnRestart SOLIDWORKS", "invokable": true}
})));
}
#[test]
fn a_crash_reporter_says_a_crash_happened_it_does_not_merely_mention_solidworks() {
for yes in [
"SOLIDWORKS Design has stopped working",
"SOLIDWORKS Error Report",
"SOLIDWORKS has stopped responding",
"SOLIDWORKS Document Recovery",
] {
assert!(is_crash_reporter_candidate(yes), "{yes} should be a candidate");
}
for no in [
"SOLIDWORKS crash on save — how to fix it - Google Chrome",
"Restart SOLIDWORKS to finish installing the update",
"SOLIDWORKS Installation Manager — restart required",
"Re: SOLIDWORKS keeps crashing - Outlook",
"SOLIDWORKS",
"Save changes to Part1?",
"gallia - Google Chrome",
] {
assert!(!is_crash_reporter_candidate(no), "{no} must NOT be a candidate");
}
}
#[test]
fn stages_ramp_in_order_and_carry_advice() {
let ramp = [
Stage::Invoked,
Stage::LauncherStarted,
Stage::RunBegin,
Stage::HelperSignIn,
Stage::SldworksUp,
Stage::Attaching,
Stage::Attached,
];
for pair in ramp.windows(2) {
assert!(pair[0] < pair[1], "{:?} must precede {:?}", pair[0], pair[1]);
}
for s in ramp {
assert!(!s.name().is_empty());
assert!(s.advice().len() > 20, "{s:?} needs advice a caller can act on");
}
assert_eq!(Stage::RunBegin.name(), "runBegin");
}
#[test]
fn a_launcher_log_is_ours_only_when_the_evidence_says_so() {
use crate::launchos::{stamp_as_utc_epoch, stamp_of, LogFile};
let log = |stamp: Option<u64>, created: Option<u64>| LogFile {
path: "x".into(),
created_at: created,
stamp,
};
// A REALISTIC pair: the stamp is local time, the fs time is UTC, and
// they agree to within a timezone.
let s1 = stamp_of("20260904172307_V6R2026x.HF4.13.log").unwrap();
let s0 = stamp_of("20260904121543_V6R2026x.HF4.13.log").unwrap();
let utc1 = stamp_as_utc_epoch(s1).unwrap();
assert!(log(Some(s1), Some(utc1)).is_newer_than(Some(s0), Some(utc1 - 18_000)));
assert!(!log(Some(s0), Some(utc1 - 18_000)).is_newer_than(Some(s1), Some(utc1)));
// NO BASELINE = NO. This is what a FAILED directory listing produced,
// and the old rule made every old log ours — five of the nine on the
// real machine contain `Run Begin`.
assert!(!log(Some(s1), Some(utc1)).is_newer_than(None, None));
assert!(!log(None, Some(100)).is_newer_than(None, None));
// EITHER clock may answer yes. Across a daylight-saving repeat the
// local stamp goes BACKWARDS while the filesystem does not.
let dst_ours = stamp_of("20261101013000_V6R2026x.HF4.13.log").unwrap();
let dst_base = stamp_of("20261101015900_V6R2026x.HF4.13.log").unwrap();
let dst_utc = stamp_as_utc_epoch(dst_ours).unwrap();
assert!(
log(Some(dst_ours), Some(dst_utc + 1_800)).is_newer_than(Some(dst_base), Some(dst_utc)),
"the filesystem is monotonic even when the wall clock is not"
);
// Fall back to fs time when there is no stamp; refuse when neither reads.
assert!(log(None, Some(100)).is_newer_than(None, Some(50)));
assert!(!log(None, Some(50)).is_newer_than(None, Some(100)));
assert!(!log(None, None).is_newer_than(None, Some(50)));
// A stamp the filesystem contradicts is NOT trusted — one log copied
// from another machine would otherwise poison a directory nothing prunes.
assert!(log(Some(s1), Some(0)).trusted_stamp().is_none(), "a stamp 56 years off its file");
assert!(log(Some(s1), Some(utc1)).trusted_stamp().is_some());
// ...and neither is one from the future.
let future = 20990101000000u64;
assert!(log(Some(future), Some(crate::launchos::epoch_secs())).trusted_stamp().is_none());
assert_eq!(stamp_of("20260904172307_V6R2026x.HF4.13.log"), Some(20260904172307));
assert_eq!(stamp_of("V6R2026x.HF4.13.log"), None);
assert_eq!(stamp_of("2026090417230_short.log"), None, "13 digits is not a stamp");
// The civil-date maths, against a known instant.
assert_eq!(stamp_as_utc_epoch(19700101000000), Some(0));
assert_eq!(stamp_as_utc_epoch(20260904172307), Some(1_788_542_587));
assert_eq!(stamp_as_utc_epoch(20261399000000), None, "month 13 is not a date");
}
#[test]
fn only_the_two_routes_that_can_say_what_they_started_may_ever_kill() {
assert!(route_is_attributable("shellExecute"));
assert!(route_is_attributable("powershell"));
assert!(!route_is_attributable("explorer"), "the shell forwards the link and returns nothing");
assert!(!route_is_attributable("cmdStart"), "`start` returns as soon as it has handed off");
// ...and the attributable ones are tried FIRST.
assert_eq!(ROUTES[0], "shellExecute");
assert_eq!(ROUTES[1], "powershell");
assert!(ROUTES.iter().all(|r| is_route(r)));
}
/// THE MEASURED CONNECTED-EDITION LAUNCH (2026-10-14): the invoke reported
/// its root (pid 53900, created 1792014529, `startProcessPassThru`);
/// sldworks.exe came up as pid 70644, created 1792014533, with ppid 40520 —
/// the launcher chain re-parents, so no parent walk reaches the root.
fn measured_launch() -> (Attempt, Vec<ProcInfo>) {
let mut a = unfinished_placeholder("SOLIDWORKS.lnk");
a.started_at = 1792014520;
a.invoked_at = 1792014528;
a.stage = Stage::Attached;
a.stage_at = 1792014611;
a.baseline_known = true;
a.baseline_pids = vec![6076, 90552];
a.attribution = Some(Attribution { pid: 53900, created_at: Some(1792014529), source: "startProcessPassThru".into() });
a.chain = vec![p(53900, "CATSTART.exe", Some(1792014529), None)];
a.chain_last_seen = vec![(53900, 1792014529, 1792014540)];
let census = vec![
p(6076, "3DEXPERIENCELauncher.exe", Some(1791900000), None),
p(40520, "SWXDesktopHelper.exe", Some(1792014531), None),
p(70644, "SLDWORKS.exe", Some(1792014533), Some(40520)),
];
(a, census)
}
#[test]
fn the_launched_solidworks_is_attributed_by_the_launch_window_when_ancestry_cannot_reach_it() {
let (a, census) = measured_launch();
assert_eq!(
solidworks_attribution(&a, &census, Some(70644), a.stage_at),
Ok((70644, 1792014533, "newProcessInLaunchWindow")),
"the one new sldworks.exe of the window, and the one COM is attached to"
);
// COM is attached to some OTHER process: no claim.
assert!(solidworks_attribution(&a, &census, Some(99999), a.stage_at).unwrap_err().contains("not the one COM is attached to"));
// TWO new SOLIDWORKS in the window: never a guess.
let mut two = census.clone();
two.push(p(70700, "SLDWORKS.exe", Some(1792014560), Some(40520)));
assert!(solidworks_attribution(&a, &two, None, a.stage_at).unwrap_err().contains("2 sldworks.exe"));
// One that was ALREADY running (in the baseline) is never new.
let mut pre = a.clone();
pre.baseline_pids.push(70644);
assert!(solidworks_attribution(&pre, &census, Some(70644), pre.stage_at).is_err());
// Created AFTER the attach (the user's own, later): outside the window.
let mut late = census.clone();
late[2] = p(70644, "SLDWORKS.exe", Some(1792014700), Some(40520));
assert!(solidworks_attribution(&a, &late, Some(70644), a.stage_at).is_err());
// Created BEFORE the attempt started: outside the window.
late[2] = p(70644, "SLDWORKS.exe", Some(1792014000), Some(40520));
assert!(solidworks_attribution(&a, &late, Some(70644), a.stage_at).is_err());
// No baseline: nothing can be called new.
let mut blind = a.clone();
blind.baseline_known = false;
assert!(solidworks_attribution(&blind, &census, Some(70644), blind.stage_at).unwrap_err().contains("could not be taken"));
}
#[test]
fn ancestry_is_tried_first() {
let (a, mut census) = measured_launch();
// sldworks.exe whose parent IS our recorded root, alive with the same identity.
census.push(p(53900, "CATSTART.exe", Some(1792014529), None));
census[2] = p(70644, "SLDWORKS.exe", Some(1792014533), Some(53900));
assert_eq!(solidworks_attribution(&a, &census, Some(70644), a.stage_at), Ok((70644, 1792014533, "ancestry")));
// Two descend from our chain: refused, even though the window would pick one.
census.push(p(70645, "SLDWORKS.exe", Some(1792014534), Some(53900)));
assert!(solidworks_attribution(&a, &census, None, a.stage_at).unwrap_err().contains("descend from this launch's chain"));
}
}