app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.9.5 source: adversary passes 42+43 folded (gated by default, log bound to session, a window's words count); manifest…
b63f99e
20d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
//! ADVERSARIAL REVIEW OF 0.9.3 — pass forty-two.
//!
//! readiness.rs, perflog.rs and session_history.rs are NOT cfg(windows): all
//! three compile and run here, driven by the simulator's `mock-startup-*`
//! faults and `SWX_PERF_LOG`. The COM side of `startup_state` (the real
//! `StartupProcessCompleted` call in com.rs) and LiveOs are cfg(windows) and
//! were NOT RUN; nothing below is a claim about them.
//!
//! WHAT CHECKS OUT
//! * The pass-41 ledger fixes: multiset-max merge; per-line `seq` (max + 1);
//! trim only past 2x cap, the removed lines archived to
//! `ledger-archive-<stamp>[-n].jsonl` (collision suffix) with a
//! `ledger_archived` line. Overlapping `seq` ranges can only arise from two
//! 0.9.3+ ledgers in two roots; the legacy roots are never written by 0.9.3,
//! so that needs a contrived configuration. After any merge `max + 1` is
//! still unique going forward.
//! * Once ready, a session (by pid) stays ready, so an on-demand LOAD_ADDIN
//! AFTER the bridge has seen it ready does not flip it back.
//! * The prime: emptiness is checked before and after; `mark` runs before the
//! close, so a session is primed at most once; `reset_workspace` closes only
//! bridge-created documents, so a user document that appears between the
//! check and the create (TOCTOU) is not closed — and the "not empty
//! afterwards" check then stops the ladder. A prime whose close FAILS leaves
//! the bridge's own scratch part (which has a file since 0.6.0) open and
//! stops: safe, but reported only as "not empty afterwards", not as "the
//! bridge's own part is still open; drain it".
//! * session-history.json is keyed pid:creationTime on disk, so everHadDocument
//! survives a com-host respawn and cannot transfer to a reused pid.
mod common;
use common::*;
use serde_json::{json, Value};
use std::time::{Duration, SystemTime};
fn s(v: &Value) -> String {
serde_json::to_string_pretty(v).unwrap_or_default()
}
fn a_visible_machine(tag: &str) -> TempDir {
let d = TempDir::new(tag);
std::fs::write(d.join("windows.json"), "[]").unwrap();
d
}
fn session(scratch: &TempDir, docs: Value) {
std::fs::write(
scratch.join("mock-solidworks-open-docs.json"),
json!({"running": true, "untitled": 1, "active": Value::Null, "docs": docs, "files": {}}).to_string(),
)
.unwrap();
}
/// SEVERITY 1 (for this release's purpose) — VERBS THAT ISSUE THE SAME COM
/// COMMANDS THE CRASH LOG NAMES ARE NOT ON THE GATE.
///
/// `STARTUP_GATED` lists 15 verbs. Not on it, and each one touches a document:
///
/// * `swx_show_part` / `_assembly` / `_drawing {path}` — OPEN a document: the
/// same `open` host call `swx_open` makes, and `swx_open` IS gated.
/// `session_history::note_reply` counts them as "made one". (I first wrote
/// that they CREATE one; reading `verbs3::show` corrected me.)
/// * `swx_export` and the ten `swx_export_*` — `SaveAs3` on the active
/// document. The 2026-10-01 crash log reads `<PROCMEM> CMD:DocSave`
/// immediately before `<RUNTIME>` and `<SWERR_REPORT>`.
/// * `swx_com_eval` — arbitrary COM.
/// * `swx_set_parameter`, `swx_props_set`, `swx_rebuild_check`, `swx_bom`,
/// `swx_pack_and_go`, `swx_activate_document`, `swx_set_preference`.
///
/// So the measured crash — a document command during add-in load — is still
/// reachable through a verb whose name does not happen to be on a list. The list
/// is the wrong shape: gate every `exec`/`write` verb that reaches COM, and
/// exempt the few that must work during startup (`swx_status`, the window
/// rungs). `tests/wiring.rs` can then check the exemption list instead of the
/// gate list, so a new verb is gated by default.
///
/// Sev-1 because it defeats the release's own premise — the gate exists to stop
/// exactly this crash. 0.9.3 should still stay published: 0.9.2 gated nothing.
#[test]
fn every_verb_that_creates_or_saves_a_document_waits_for_startup() {
let scratch = TempDir::new("adv40-ungated");
std::fs::write(
scratch.join("mock-solidworks-open-docs.json"),
json!({"running": true, "untitled": 1, "active": "C:\\Users\\drew\\work\\bracket.SLDPRT",
"docs": [{"docType": "part", "path": "C:\\Users\\drew\\work\\bracket.SLDPRT",
"title": "bracket.SLDPRT", "saved": true, "dirty": false}],
"files": {}}).to_string(),
)
.unwrap();
std::fs::write(scratch.join("mock-startup-incomplete"), "").unwrap();
let machine = a_visible_machine("adv40-ungated-m");
let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);
b.call("swx_start", json!({}));
let gated = b.call("swx_new_document", json!({"type": "part"}));
assert!(gated["errorCode"] == json!("host_app_starting"), "precondition: the gate is active: {}", s(&gated));
let mut through: Vec<String> = Vec::new();
for (verb, args) in [
("swx_show_part", json!({"path": "C:\\Users\\drew\\work\\other.SLDPRT"})),
("swx_export_step", json!({"outputPath": "C:\\out\\bracket.step"})),
("swx_com_eval", json!({"expr": "ActiveDoc.GetTitle", "reason": "probe"})),
] {
let r = b.call(verb, args);
if r["errorCode"] != json!("host_app_starting") {
through.push(format!("{verb}: {}", r["errorCode"].as_str().unwrap_or(if r["success"] == json!(true) { "SUCCESS" } else { "other" })));
}
}
assert!(
through.is_empty(),
"with SOLIDWORKS still starting, these document-touching verbs were NOT held at the \
gate: {through:?}. swx_show_* {{path}} opens a document (swx_open's own call) and the export verbs SaveAs3 (the crash \
log's CMD:DocSave) — the measured 2026-10-01 crash, through a verb not on a 15-name list. \
Gate by default; list the exemptions."
);
}
/// SEVERITY 2 — AN ADD-IN LOADED ON DEMAND BEFORE THE BRIDGE FIRST LOOKS MAKES A
/// FULLY STARTED SESSION "STARTING" FOREVER — AND UNQUITTABLE.
///
/// `perflog::startup_complete` says `Some(false)` whenever the last `LOAD_ADDIN`
/// line is newer than the last 3DEXPERIENCE marker. That marker is written once
/// per start. A user who starts SOLIDWORKS by hand and later enables an add-in
/// (Toolbox, Simulation) writes a `LOAD_ADDIN` after it; if the bridge first sees
/// the session after that, the log says "still loading" for the rest of the
/// session. READY requires every readable signal to agree, so COM's `true` is
/// out-voted, `readinessTimedOut` arrives at five minutes, and it stays
/// `starting` — by design, "never promoted to ready by the clock".
///
/// And `swx_stop`, `swx_close_all_safely` and `swx_shutdown` are on the gate.
/// So the bridge cannot close or quit a perfectly healthy session — the same
/// dead end as a session genuinely wedged at startup behind a dialog, where the
/// WINDOW-MESSAGE rungs (which need no COM and no readiness) are exactly what a
/// quit would use. Fail-safe, but it puts "ask the user to close it" back as the
/// only way out.
///
/// Fix: bind the log to the session (below), and let a positive COM
/// `StartupProcessCompleted` on a session the log cannot place win; never gate
/// the quit's window rungs on readiness.
#[test]
fn an_add_in_loaded_after_the_marker_does_not_make_a_started_session_unready_forever() {
let scratch = TempDir::new("adv40-ondemand");
session(&scratch, json!([]));
let log = scratch.join("SolidWorksPerformance.log");
std::fs::write(
&log,
"<LOAD_ADDIN> SolidWorks Toolbox\n<LOAD_ADDIN> 3DEXPERIENCE\n<3DXLOAD_TIME>24000</3DXLOAD_TIME>\n\
<LOAD_ADDIN> SOLIDWORKS Simulation\n",
)
.unwrap();
// BUILDER NOTE (0.9.5): the rule that tells an on-demand LOAD_ADDIN from a
// start still loading is "COM StartupProcessCompleted=true, a marker for this
// session, then a LOAD_ADDIN and no log activity for ON_DEMAND_QUIET_SECS
// (60 s)". A log written this instant cannot meet that — it is exactly what a
// start still loading add-ins looks like — so the fixture's log was last
// written two minutes ago, after the (simulated, hour-old) process started.
std::fs::File::options()
.write(true)
.open(&log)
.unwrap()
.set_modified(SystemTime::now() - Duration::from_secs(120))
.unwrap();
let logp = log.to_string_lossy().to_string();
let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
b.call("swx_start", json!({}));
let st = b.call("swx_status", json!({}));
let r = b.call("swx_new_document", json!({"type": "part"}));
let stop = b.call("swx_stop", json!({"userAuthorized": true, "reason": "the user asked to quit"}));
assert!(
r["readiness"]["startupProcessCompleted"] == json!(true) || st.to_string().contains("\"startupProcessCompleted\":true"),
"precondition: COM says the start completed: {} / {}", s(&r), s(&st)
);
assert!(
r["errorCode"] != json!("host_app_starting") && stop["errorCode"] != json!("host_app_starting"),
"COM says SOLIDWORKS finished starting and the 3DEXPERIENCE marker is in the log, but one \
add-in loaded on demand after it, so the log reads \"still loading\" for the rest of the \
session: document verbs AND swx_stop are refused, and the five-minute timeout never \
promotes it. A healthy session the bridge cannot close or quit.\n new_document: {}\n \
stop: {}",
s(&r),
s(&stop)
);
}
/// SEVERITY 2 — THE PERFORMANCE LOG IS NOT BOUND TO THIS SESSION.
///
/// `perflog::path()` takes the most recently written log; `startup_complete`
/// reads its tail. Nothing compares the log with the running process — no pid,
/// no creation time, not even file mtime >= process start. A log whose last
/// entries are a COMPLETED earlier start therefore reads `Some(true)` for a
/// session that has not written a line yet. With COM unable to answer — exactly
/// the early-startup window — that one stale witness is the whole verdict, and
/// the session is READY at second 0: the 8-second mistake, with no wait at all.
/// The bridge already knows the process creation time (attribution, session
/// history); a log last modified before it cannot speak for this session.
///
/// ON THE GRACE (asked to argue it): `ready` after 60 s with neither signal is
/// the same class of guess as the 8 s that crashed, at 2.5x the one measured
/// platform load (24 s after a 29.7 s attach). It is defensible only because it
/// is labelled (`basedOn: graceAfterFirstSeen`) — but the verbs read `ready`,
/// not `basedOn`, and the module's own rule says the clock never promotes a
/// start to ready. I would keep the grace for READ verbs and keep document verbs
/// refused with `startingUnverifiable`, so the clock cannot authorise a crash.
#[test]
fn a_log_from_an_earlier_session_cannot_vouch_for_this_one() {
let scratch = TempDir::new("adv40-stale");
session(&scratch, json!([]));
std::fs::write(scratch.join("mock-startup-unreadable"), "").unwrap();
let log = scratch.join("SolidWorksPerformance.log");
std::fs::write(&log, "<LOAD_ADDIN> 3DEXPERIENCE\n<3DXLOAD_TIME>24000</3DXLOAD_TIME>\n").unwrap();
std::fs::File::options()
.write(true)
.open(&log)
.unwrap()
.set_modified(SystemTime::now() - Duration::from_secs(86_400))
.unwrap();
let logp = log.to_string_lossy().to_string();
let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
b.call("swx_start", json!({}));
let r = b.call("swx_new_document", json!({"type": "part"}));
let st = b.call("swx_status", json!({}));
assert!(
st.to_string().contains("\"startupProcessCompleted\":null"),
"precondition: COM cannot answer: {}", s(&st)
);
assert!(
r["errorCode"] == json!("host_app_starting"),
"COM cannot say whether SOLIDWORKS has started, and the only other witness is a \
performance log last written a DAY ago — a previous session's completed start. It was \
taken as this session's: swx_new_document ran at once, with no grace. Bind the log to the \
session (mtime >= the process creation time the bridge already knows).\n {}",
s(&r)
);
}