app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
//! Find SOLIDWORKS installs and running instances (Windows).
//!
//! Detection ONLY. ab's rule for host apps is detect-by-default, install
//! on-request — and SOLIDWORKS has no silent free installer anyway (desktop
//! trial serials come from a reseller), so the bridge reports absence with next
//! steps instead of ever attempting an install itself.
//!
//! Three signals, cheapest and most reliable first:
//! 1. the `App Paths\SLDWORKS.exe` registry key (HKLM then HKCU),
//! 2. the known install-path shapes under Program Files,
//! 3. `HKLM\SOFTWARE\SolidWorks` subkeys for the version census,
//! plus a `tasklist` check for whether it is running right now.
use serde_json::{json, Value};
#[derive(Clone, Debug, Default)]
pub struct Detection {
pub installed: bool,
pub exe: Option<String>,
pub registry_versions: Vec<String>,
pub running: bool,
/// Is the 3DEXPERIENCE Launcher present on this machine? A SECOND,
/// independent signal for [`Detection::edition`] — the install path alone is
/// a substring heuristic, and getting the edition wrong is what puts the
/// "launch me from the Platform" modal back on the user's screen.
pub launcher_present: bool,
/// The Platform-created shortcut that STARTS SOLIDWORKS Connected, if this
/// machine has one. See [`find_shortcut`] — this is the field that turned
/// "cannot be launched programmatically" into a launch that works.
pub shortcut: Option<String>,
}
impl Detection {
pub fn to_json(&self) -> Value {
let edition = self.edition();
json!({
"installed": self.installed,
"exe": self.exe,
"registryVersions": self.registry_versions,
"running": self.running,
"platform": std::env::consts::OS,
"edition": edition,
"canLaunchProgrammatically": self.can_launch(),
"shortcut": self.shortcut,
"launchRoute": self.launch_route().name(),
// CLASSIC DESKTOP RELEASES in the registry (0.10.0): SOLIDWORKS 2021
// is major version 29. Launched through COM (the exe), with no
// 3DEXPERIENCE sign-in; see docs/COMPATIBILITY.md.
"registryYears": registry_years(&self.registry_versions),
"classicYears": classic_years_from(self.exe.as_deref(), &self.registry_versions, edition),
})
}
/// HOW this bridge may start SOLIDWORKS, if it may at all.
pub fn launch_route(&self) -> LaunchRoute {
launch_route_from(self.installed, self.edition(), self.shortcut.as_deref())
}
/// Which SOLIDWORKS this is: `"connected"`, `"desktop"` or `"unknown"`.
///
/// The distinction is not cosmetic — it decides whether `swx_start` may
/// launch the application at all.
///
/// LIVE FINDING (2026-09-02): on **SOLIDWORKS Connected** (the
/// 3DEXPERIENCE-hosted editions, which includes "Design Professional for
/// Makers"), `CoCreateInstance(SldWorks.Application)` starts a process that
/// immediately puts up *"SOLIDWORKS Design must be launched from the
/// 3DEXPERIENCE Platform or from the desktop shortcut created from the
/// Platform"* and never becomes a drivable COM server. Launching is
/// human-gated by Dassault. Classic desktop SOLIDWORKS is unaffected, so the
/// launch path stays — behind this check.
///
/// THREE independent signals, because the first version of this was a single
/// substring test on an install path nobody had ever observed on the target
/// machine, and a wrong answer means firing the launch that produces that
/// modal. `"desktop"` now requires POSITIVE evidence; anything unrecognised
/// is `"unknown"`, and unknown does not launch.
pub fn edition(&self) -> &'static str {
edition_from(self.exe.as_deref(), &self.registry_versions, self.launcher_present)
}
/// May this bridge start SOLIDWORKS itself?
///
/// ONLY a positively-identified classic desktop install. An unrecognised
/// layout answers NO: the cost of refusing a launch we could have made is
/// one sentence asking the user to click their own shortcut, and the cost of
/// making a launch we should have refused is a modal dialog and a dead
/// process on their machine. Those are not close.
pub fn can_launch(&self) -> bool {
self.installed && self.edition() == "desktop"
}
}
/// The edition decision as a PURE function, so both install layouts can be
/// unit-tested on any host — the previous version could only be exercised on a
/// Windows box with SOLIDWORKS actually installed, which is to say never.
///
/// Evidence, in order:
/// 1. an install path or registry entry naming 3DEXPERIENCE -> connected
/// 2. the 3DEXPERIENCE Launcher present on the machine -> connected
/// 3. an install path under `SOLIDWORKS Corp` (the classic
/// desktop layout, and the ONLY positive evidence for it) -> desktop
/// 4. anything else -> unknown
pub fn edition_from(
exe: Option<&str>,
registry_versions: &[String],
launcher_present: bool,
) -> &'static str {
let up = exe.unwrap_or("").to_uppercase();
let registry_says_connected = registry_versions
.iter()
.any(|v| v.to_uppercase().contains("3DEXPERIENCE") || v.to_uppercase().contains("CONNECTED"));
if up.contains("3DEXPERIENCE") || registry_says_connected || launcher_present {
return "connected";
}
if up.contains("SOLIDWORKS CORP") {
return "desktop";
}
// THE CLASSIC REGISTRY LAYOUT (0.10.0, issue #1/#2: SOLIDWORKS 2021):
// `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS 2021`, a year and nothing else, with no
// 3DEXPERIENCE marker anywhere (checked above) and an exe found. UNMEASURED
// on a classic machine; it is the documented layout of the desktop installer.
if exe.is_some() && !registry_years(registry_versions).is_empty() {
return "desktop";
}
if exe.is_none() {
return "unknown";
}
// A path we do not recognise — including `Dassault Systemes\...` without any
// other marker, which is where the Connected editions live. Not proof of
// either, so: unknown, which means "do not launch".
"unknown"
}
/// Every release year a plain `SOLIDWORKS <year>` registry key names:
/// "SOLIDWORKS 2021" -> 2021. A FACT about the registry, not a claim about the
/// edition — MEASURED 2026-10-13: the 3DEXPERIENCE (Connected) 2026 install
/// writes "SOLIDWORKS 2026" too, so this alone is not "a classic install".
pub fn registry_years(registry_versions: &[String]) -> Vec<u32> {
let mut out: Vec<u32> = registry_versions
.iter()
.filter_map(|v| {
let rest = v.trim().strip_prefix("SOLIDWORKS ").or_else(|| v.trim().strip_prefix("SolidWorks "))?;
let year: u32 = rest.trim().parse().ok()?;
(2000..2100).contains(&year).then_some(year)
})
.collect();
out.sort();
out.dedup();
out
}
/// The release years tied to 3DEXPERIENCE / Connected: a registry entry or an
/// install path that names the platform and a year ("SOLIDWORKS 3DEXPERIENCE
/// R2026x" -> 2026).
pub fn connected_years(exe: Option<&str>, registry_versions: &[String]) -> Vec<u32> {
fn years_in(s: &str) -> Vec<u32> {
let b = s.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i + 4 <= b.len() {
let run = &b[i..i + 4];
let boundary_before = i == 0 || !b[i - 1].is_ascii_digit();
let boundary_after = i + 4 == b.len() || !b[i + 4].is_ascii_digit();
if run.iter().all(|c| c.is_ascii_digit()) && boundary_before && boundary_after {
if let Ok(y) = std::str::from_utf8(run).unwrap_or("").parse::<u32>() {
if (2000..2100).contains(&y) {
out.push(y);
}
}
i += 4;
} else {
i += 1;
}
}
out
}
let marked = |t: &str| {
let u = t.to_uppercase();
u.contains("3DEXPERIENCE") || u.contains("CONNECTED")
};
let mut out: Vec<u32> = registry_versions.iter().filter(|v| marked(v)).flat_map(|v| years_in(v)).collect();
if let Some(e) = exe.filter(|e| marked(e)) {
out.extend(years_in(e));
}
out.sort();
out.dedup();
out
}
/// The CLASSIC desktop releases on this machine: registry years with no
/// 3DEXPERIENCE entry or install path for the same release. On a Connected
/// machine whose platform marker names no year (the launcher alone), no year
/// can be shown to be classic, so none is claimed. MEASURED 2026-10-13: 0.10.0
/// reported `classicYears: [2026]` on the 3DEXPERIENCE 2026 install, which
/// read as a classic install that is not there. The EDITION decision is not
/// made here and did not change.
pub fn classic_years_from(exe: Option<&str>, registry_versions: &[String], edition: &str) -> Vec<u32> {
let all = registry_years(registry_versions);
if edition == "desktop" {
return all;
}
let connected = connected_years(exe, registry_versions);
if edition == "connected" && connected.is_empty() {
return Vec::new();
}
all.into_iter().filter(|y| !connected.contains(y)).collect()
}
/// How this bridge may start SOLIDWORKS.
///
/// LIVE FINDING (2026-09-03) — THE LAUNCH ANSWER. The earlier conclusion that
/// SOLIDWORKS Connected "cannot be launched programmatically" was drawn from the
/// wrong experiment: we called `CoCreateInstance` on `SldWorks.Application`,
/// which starts `sldworks.exe` DIRECTLY, and the application correctly refused
/// because it had not been given a tenant or a registry to talk to. Reading the
/// Start-Menu shortcut the Platform installs shows what the sanctioned start
/// actually is, and it is not sldworks.exe at all:
///
/// ```text
/// CATSTART.exe -run "SWXDesktopLauncher.exe"
/// -object "--AppName=\"SWXCSWK_AP\" -tenant=<TENANT> -monoapp
/// -3DRegistryURL=https://<region>-makers-registry.3dexperience.3ds.com"
/// -nowindow
/// ```
///
/// So the refusal was ours: we bypassed the launcher chain. Invoking the
/// USER'S OWN SHORTCUT runs exactly the command Dassault intends, with the
/// tenant and registry arguments the Platform baked into it — nothing is
/// reverse-engineered and no argument is invented. That is why this bridge
/// invokes the `.lnk` rather than assembling a CATSTART command line itself: the
/// shortcut is the sanctioned artefact, and a hand-built command line would be a
/// guess at somebody's tenant.
#[derive(Clone, Debug, PartialEq)]
pub enum LaunchRoute {
/// Classic desktop SOLIDWORKS: `CoCreateInstance` yields a drivable COM
/// server on its own.
Com,
/// SOLIDWORKS Connected: invoke this shortcut and wait for the COM server to
/// appear. Cold start runs minutes, not seconds.
PlatformShortcut(String),
/// Nothing this bridge may do — ask the user to start it.
Blocked,
}
impl LaunchRoute {
pub fn name(&self) -> &'static str {
match self {
LaunchRoute::Com => "com",
LaunchRoute::PlatformShortcut(_) => "platformShortcut",
LaunchRoute::Blocked => "blocked",
}
}
}
/// The route decision as a PURE function, so every branch is reachable from a
/// test on any host.
///
/// `installed` is deliberately NOT required for the shortcut route: a shortcut
/// that exists IS evidence of an install, and the exe globs miss the Connected
/// layouts often enough that requiring both would refuse launches on exactly the
/// machines this route exists for.
pub fn launch_route_from(
installed: bool,
edition: &'static str,
shortcut: Option<&str>,
) -> LaunchRoute {
// A positively-identified desktop install attaches directly, which is
// faster and needs no external process. Prefer it.
if installed && edition == "desktop" {
return LaunchRoute::Com;
}
match shortcut.filter(|s| !s.is_empty()) {
Some(s) => LaunchRoute::PlatformShortcut(s.to_string()),
None => LaunchRoute::Blocked,
}
}
/// An explicit override, for a machine whose shortcut lives somewhere these
/// globs do not look. Also how the Linux test suite drives the shortcut route.
pub const ENV_SHORTCUT: &str = "SWX_SOLIDWORKS_SHORTCUT";
/// Folders the Platform puts its Start-Menu group in. The `*` matches the
/// release component (`... 3DEXPERIENCE R2026x`).
pub const SHORTCUT_DIR_GLOBS: &[&str] = &[
r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS*",
r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS*",
];
/// Individual shortcuts, checked before the folder scan. The Platform also
/// offers to drop one on the desktop, and the refusal modal names it
/// ("...or from the desktop shortcut created from the Platform").
pub const SHORTCUT_FILE_GLOBS: &[&str] = &[
r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS Design.lnk",
r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Dassault Systemes SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS Design.lnk",
r"%USERPROFILE%\Desktop\SOLIDWORKS Design.lnk",
r"%PUBLIC%\Desktop\SOLIDWORKS Design.lnk",
];
/// Names in the Start-Menu group that are NOT the application. Launching one of
/// these would run an uninstaller or a side tool — so the ranking rejects rather
/// than guesses, and `Blocked` (which asks the user) is the fallback.
const NOT_THE_APP: &[&str] = &[
"uninstall", "installation", "install manager", "readme", "release notes", "license",
"administrat", "rx", "explorer", "toolbox", "settings", "manage", "utilit", "sldim",
"composer", "visualize", "electrical", "inspection", "pdm", "simulation", "documentation",
"help", "what's new", "tutorial", "check", "task scheduler", "language",
];
/// Which of several candidate shortcuts is the one that starts SOLIDWORKS.
///
/// Pure and ranked rather than "first match": a Start-Menu group holds an
/// uninstaller and a pile of side tools alongside the application, and picking
/// the wrong one would run the wrong program on somebody's machine.
pub fn pick_shortcut(candidates: &[String]) -> Option<String> {
let score = |p: &String| -> Option<i32> {
let file = p.rsplit(|c| c == '\\' || c == '/').next().unwrap_or("").to_lowercase();
if !file.ends_with(".lnk") {
return None;
}
let stem = file.trim_end_matches(".lnk");
if !stem.contains("solidworks") {
return None;
}
if NOT_THE_APP.iter().any(|bad| stem.contains(bad)) {
return None;
}
// Exact, then the Connected application name, then anything left —
// shorter names first, because the plain application is always the
// shortest "SOLIDWORKS ..." entry in the group.
Some(match stem {
"solidworks design" => 0,
"solidworks" => 1,
s if s.contains("design") => 2,
s => 3 + s.len() as i32,
})
};
candidates
.iter()
.filter_map(|c| score(c).map(|s| (s, c)))
.min_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)))
.map(|(_, c)| c.clone())
}
/// Find the shortcut that starts SOLIDWORKS Connected, if there is one.
pub fn find_shortcut() -> Option<String> {
if let Ok(explicit) = std::env::var(ENV_SHORTCUT) {
if !explicit.is_empty() {
// An explicit override is honoured whether or not it exists yet, so
// a wrong value fails loudly at launch instead of silently falling
// back to a different shortcut.
return Some(explicit);
}
}
let mut candidates: Vec<String> =
SHORTCUT_FILE_GLOBS.iter().flat_map(|g| expand_glob(g)).collect();
if let Some(found) = pick_shortcut(&candidates) {
return Some(found);
}
// Nothing at the known names: scan the group folder itself.
for dir_glob in SHORTCUT_DIR_GLOBS {
for dir in expand_dir_glob(dir_glob) {
if let Ok(entries) = std::fs::read_dir(&dir) {
for e in entries.flatten() {
candidates.push(e.path().to_string_lossy().to_string());
}
}
}
}
pick_shortcut(&candidates)
}
/// [`expand_glob`] for a pattern whose final component is a DIRECTORY.
/// `expand_glob` requires the resolved candidate to be a file, so it cannot
/// answer "which release folder is on this machine".
pub fn expand_dir_glob(pattern: &str) -> Vec<String> {
let expanded = expand_env(pattern);
let sep = |c: char| c == '\\' || c == '/';
let star = match expanded.find('*') {
None => {
return if std::path::Path::new(&expanded).is_dir() { vec![expanded] } else { vec![] }
}
Some(i) => i,
};
let (head, tail) = (&expanded[..star], &expanded[star + 1..]);
if tail.contains(|c| sep(c)) {
return Vec::new(); // one wildcard component, and it is the last one
}
let (parent, prefix) = match head.rfind(sep) {
Some(i) => (&head[..i], &head[i + 1..]),
None => ("", head),
};
let mut hits = Vec::new();
if let Ok(entries) = std::fs::read_dir(if parent.is_empty() { "." } else { parent }) {
for e in entries.flatten() {
if !e.path().is_dir() {
continue;
}
let name = e.file_name().to_string_lossy().to_lowercase();
if name.starts_with(&prefix.to_lowercase())
&& name.ends_with(&tail.to_lowercase())
&& name.len() >= prefix.len() + tail.len()
{
hits.push(e.path().to_string_lossy().to_string());
}
}
}
hits.sort();
hits
}
/// Where the 3DEXPERIENCE Launcher lands. Its presence means this machine is
/// set up for the Connected editions whatever the SOLIDWORKS path looks like.
pub const LAUNCHER_GLOBS: &[&str] = &[
r"%ProgramFiles%\Dassault Systemes\3DEXPERIENCE Launcher",
r"%LOCALAPPDATA%\3DEXPERIENCE Launcher",
r"%ProgramFiles(x86)%\Dassault Systemes\3DEXPERIENCE Launcher",
];
/// Is any of the launcher locations present?
pub fn launcher_present() -> bool {
LAUNCHER_GLOBS.iter().any(|g| std::path::Path::new(&expand_env(g)).exists())
}
/// One detection pass. Cheap enough to run on every `swx_status`.
pub fn detect() -> Detection {
#[cfg(windows)]
{
windows_impl::detect()
}
#[cfg(not(windows))]
{
// SOLIDWORKS is Windows-only; on any other host the honest answer is
// "not installed", and the bridge runs its mock backend.
//
// `find_shortcut` is still called: it honours SWX_SOLIDWORKS_SHORTCUT,
// which is how the Linux suite reaches the PlatformShortcut branch. A
// branch that only a Windows machine with SOLIDWORKS on it can enter is
// a branch nothing tests.
Detection { shortcut: find_shortcut(), ..Detection::default() }
}
}
/// Known install-path shapes. `*` matches exactly one path component (the
/// year-versioned folder SOLIDWORKS Corp uses).
pub const CANDIDATE_GLOBS: &[&str] = &[
r"%ProgramFiles%\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe",
r"%ProgramFiles%\SOLIDWORKS Corp\*\SOLIDWORKS\SLDWORKS.exe",
r"%ProgramFiles%\Dassault Systemes\SOLIDWORKS\SLDWORKS.exe",
// 3DEXPERIENCE SOLIDWORKS Connected installs under the 3DEXPERIENCE launcher:
r"%ProgramFiles%\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE*\SOLIDWORKS\SLDWORKS.exe",
r"%ProgramFiles%\Dassault Systemes\*\SOLIDWORKS\SLDWORKS.exe",
];
/// Expand `%VAR%` references; an undefined variable expands to itself so the
/// resulting path simply fails to exist instead of silently matching `\`.
pub fn expand_env(pattern: &str) -> String {
let mut out = String::new();
let mut rest = pattern;
while let Some(start) = rest.find('%') {
out.push_str(&rest[..start]);
let after = &rest[start + 1..];
match after.find('%') {
Some(end) => {
let name = &after[..end];
match std::env::var(name) {
Ok(v) => out.push_str(&v),
Err(_) => {
out.push('%');
out.push_str(name);
out.push('%');
}
}
rest = &after[end + 1..];
}
None => {
out.push('%');
out.push_str(after);
rest = "";
}
}
}
out.push_str(rest);
out
}
/// Resolve one candidate pattern to existing files. Supports at most one `*`
/// component — which is all the SOLIDWORKS layouts need, and keeps a glob crate
/// out of the dependency list.
pub fn expand_glob(pattern: &str) -> Vec<String> {
let expanded = expand_env(pattern);
let star = match expanded.find('*') {
None => {
return if std::path::Path::new(&expanded).is_file() { vec![expanded] } else { vec![] }
}
Some(i) => i,
};
// The `*` lives inside ONE path component, which may also carry a literal
// prefix and/or suffix (e.g. `SOLIDWORKS 3DEXPERIENCE*` matches
// `SOLIDWORKS 3DEXPERIENCE R2026x`). Split the pattern at the component
// boundaries around the star, then match directory names by prefix/suffix.
let head = &expanded[..star];
let tail = &expanded[star + 1..];
let sep = |c: char| c == '\\' || c == '/';
let (parent, name_prefix) = match head.rfind(sep) {
Some(i) => (&head[..i], &head[i + 1..]),
None => ("", head),
};
let (name_suffix, rest) = match tail.find(sep) {
Some(i) => (&tail[..i], tail[i + 1..].to_string()),
None => (tail, String::new()),
};
let mut hits = Vec::new();
if let Ok(entries) = std::fs::read_dir(if parent.is_empty() { "." } else { parent }) {
for e in entries.flatten() {
if !e.path().is_dir() {
continue;
}
let name = e.file_name().to_string_lossy().to_string();
// Case-insensitive: Windows paths are, and the installers are inconsistent.
let (lname, lpre, lsuf) = (
name.to_lowercase(),
name_prefix.to_lowercase(),
name_suffix.to_lowercase(),
);
if !lname.starts_with(&lpre)
|| !lname.ends_with(&lsuf)
|| lname.len() < lpre.len() + lsuf.len()
{
continue;
}
let candidate = if rest.is_empty() {
e.path()
} else {
e.path().join(rest.replace('\\', std::path::MAIN_SEPARATOR_STR))
};
if candidate.is_file() {
hits.push(candidate.to_string_lossy().to_string());
}
}
}
hits.sort();
hits
}
#[cfg(windows)]
mod windows_impl {
use super::{expand_glob, Detection, CANDIDATE_GLOBS};
use std::os::windows::process::CommandExt;
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
use winreg::RegKey;
const APP_PATHS: &str =
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\SLDWORKS.exe";
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// The App Paths key — the most reliable signal when it is present.
fn app_paths_lookup() -> Option<String> {
for root in [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER] {
let hive = RegKey::predef(root);
if let Ok(key) = hive.open_subkey(APP_PATHS) {
if let Ok(path) = key.get_value::<String, _>("") {
if std::path::Path::new(&path).is_file() {
return Some(path);
}
}
}
}
None
}
/// `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS <year>` subkeys.
fn registry_versions() -> Vec<String> {
let hive = RegKey::predef(HKEY_LOCAL_MACHINE);
let mut out = Vec::new();
if let Ok(key) = hive.open_subkey(r"SOFTWARE\SolidWorks") {
for name in key.enum_keys().flatten() {
if name.to_uppercase().starts_with("SOLIDWORKS ") {
out.push(name);
}
}
}
out
}
fn is_running() -> bool {
std::process::Command::new("tasklist")
.args(["/FI", "IMAGENAME eq SLDWORKS.exe", "/FO", "CSV", "/NH"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout)
.to_ascii_lowercase()
.contains("sldworks.exe")) // tasklist prints lowercase
.unwrap_or(false)
}
/// A classic install's own folder: `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS
/// <year>\Setup` value "SolidWorks Folder" (newest year first). UNMEASURED.
fn classic_setup_exe(versions: &[String]) -> Option<String> {
let hive = RegKey::predef(HKEY_LOCAL_MACHINE);
let mut years = super::classic_years_from(None, versions, "unknown");
years.reverse();
for y in years {
if let Ok(key) = hive.open_subkey(format!(r"SOFTWARE\SolidWorks\SOLIDWORKS {y}\Setup")) {
if let Ok(dir) = key.get_value::<String, _>("SolidWorks Folder") {
let exe = std::path::Path::new(dir.trim_end_matches(['\\', '/'])).join("SLDWORKS.exe");
if exe.is_file() {
return Some(exe.to_string_lossy().to_string());
}
}
}
}
None
}
pub fn detect() -> Detection {
let versions = registry_versions();
let mut exe = app_paths_lookup().or_else(|| classic_setup_exe(&versions));
if exe.is_none() {
for pattern in CANDIDATE_GLOBS {
let hits = expand_glob(pattern);
if let Some(last) = hits.last() {
exe = Some(last.clone());
break;
}
}
}
Detection {
installed: exe.is_some(),
exe,
registry_versions: versions,
running: is_running(),
launcher_present: super::launcher_present(),
shortcut: super::find_shortcut(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_launch_route_prefers_com_but_falls_back_to_the_platform_shortcut() {
// THE LAUNCH ANSWER, as a decision table. A positively-identified
// desktop install attaches over COM; anything else launches through the
// shortcut the Platform installed, IF there is one; and only the absence
// of a shortcut blocks.
let lnk = r"C:\ProgramData\...\SOLIDWORKS Design.lnk";
assert_eq!(launch_route_from(true, "desktop", None), LaunchRoute::Com);
assert_eq!(
launch_route_from(true, "desktop", Some(lnk)),
LaunchRoute::Com,
"a desktop install attaches directly rather than running an external process"
);
// The case this whole route exists for.
assert_eq!(
launch_route_from(true, "connected", Some(lnk)),
LaunchRoute::PlatformShortcut(lnk.to_string()),
"SOLIDWORKS Connected CAN be started — through the launcher chain in its shortcut"
);
// `installed` is deliberately not required: the exe globs miss the
// Connected layouts, and a shortcut IS evidence of an install.
assert_eq!(
launch_route_from(false, "unknown", Some(lnk)),
LaunchRoute::PlatformShortcut(lnk.to_string())
);
// Blocked ONLY when there is nothing sanctioned to invoke.
assert_eq!(launch_route_from(true, "connected", None), LaunchRoute::Blocked);
assert_eq!(launch_route_from(false, "unknown", None), LaunchRoute::Blocked);
assert_eq!(
launch_route_from(true, "connected", Some("")),
LaunchRoute::Blocked,
"an empty shortcut string is not a shortcut"
);
}
#[test]
fn the_shortcut_ranking_never_picks_the_uninstaller() {
// A Start-Menu group holds the application next to an uninstaller and a
// pile of side tools. "First .lnk found" would run whichever the
// filesystem happened to list first — on somebody's machine.
let group = |n: &str| format!(r"C:\ProgramData\MS\Start Menu\Programs\DS SW 3DX\{n}");
let candidates: Vec<String> = [
"Uninstall SOLIDWORKS.lnk",
"SOLIDWORKS Rx 2026.lnk",
"SOLIDWORKS Tools.lnk",
"SOLIDWORKS Design.lnk",
"SOLIDWORKS Visualize.lnk",
]
.iter()
.map(|n| group(n))
.collect();
assert_eq!(
pick_shortcut(&candidates),
Some(group("SOLIDWORKS Design.lnk")),
"the application, not a tool and certainly not the uninstaller"
);
// Nothing recognisable: None, so the caller asks the user rather than
// running something arbitrary.
let junk: Vec<String> =
["Uninstall SOLIDWORKS.lnk", "SOLIDWORKS Rx.lnk", "readme.txt", "Notepad.lnk"]
.iter()
.map(|n| group(n))
.collect();
assert_eq!(
pick_shortcut(&junk),
None,
"no application shortcut is 'blocked', which asks the user — never a guess"
);
// Non-.lnk files in the folder are not launch candidates.
assert_eq!(pick_shortcut(&[group("SOLIDWORKS Design.exe")]), None);
// The plain name is accepted when that is what the group calls it.
assert_eq!(
pick_shortcut(&[group("SOLIDWORKS.lnk")]),
Some(group("SOLIDWORKS.lnk"))
);
}
#[test]
fn the_release_folder_is_found_by_wildcard() {
// `expand_glob` requires the resolved candidate to be a FILE, so it
// cannot answer "which release folder does this machine have".
let tmp = std::env::temp_dir().join(format!("swx-dirglob-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(tmp.join("Dassault Systemes SOLIDWORKS 3DEXPERIENCE R2026x")).unwrap();
std::fs::create_dir_all(tmp.join("Something Else")).unwrap();
let hits = expand_dir_glob(&format!(
"{}/Dassault Systemes SOLIDWORKS 3DEXPERIENCE*",
tmp.display()
));
assert_eq!(hits.len(), 1, "matched {hits:?}");
assert!(hits[0].ends_with("R2026x"));
assert!(expand_dir_glob(&format!("{}/Nope*", tmp.display())).is_empty());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn the_edition_decision_covers_both_real_install_layouts() {
let none: Vec<String> = vec![];
// The machine this bridge was built against: SOLIDWORKS Design
// Professional for Makers 2026, whose registry version string is the
// UNREMARKABLE "SOLIDWORKS 2026" — the path is what gives it away.
assert_eq!(
edition_from(
Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe"),
&["SOLIDWORKS 2026".to_string()],
false,
),
"connected"
);
// Classic desktop layout — the ONLY thing that earns a launch.
assert_eq!(
edition_from(
Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"),
&none,
false,
),
"desktop"
);
// ...and the launcher's presence overrides even that, because a machine
// with the 3DEXPERIENCE Launcher on it is a Connected machine.
assert_eq!(
edition_from(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), &none, true),
"connected"
);
// A registry entry naming 3DEXPERIENCE is signal on its own.
assert_eq!(
edition_from(Some(r"C:\somewhere\SLDWORKS.exe"), &["SOLIDWORKS 3DEXPERIENCE R2026x".to_string()], false),
"connected"
);
// An unrecognised path is UNKNOWN, not "desktop". This is the bit that
// matters: the previous version answered "desktop" for anything without
// 3DEXPERIENCE in it, so a layout nobody had seen would have fired the
// COM launch that produces the refusal modal.
assert_eq!(edition_from(Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS\SLDWORKS.exe"), &none, false), "unknown");
assert_eq!(edition_from(Some(r"D:\weird\SLDWORKS.exe"), &none, false), "unknown");
// THE CLASSIC REGISTRY (issue #1/#2's SOLIDWORKS 2021): a year key, an exe,
// no 3DEXPERIENCE anywhere -> desktop; with a 3DEXPERIENCE marker -> connected.
let y2021 = vec!["SOLIDWORKS 2021".to_string()];
assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, false), "desktop");
assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, true), "connected");
assert_eq!(edition_from(None, &y2021, false), "unknown", "no exe: nothing to launch");
assert_eq!(registry_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);
// classicYears (0.10.1): MEASURED 2026-10-13, the 3DEXPERIENCE 2026 install
// also writes "SOLIDWORKS 2026" — that year is not a classic install.
let x2026 = r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe";
let reg26 = vec!["SOLIDWORKS 2026".to_string()];
assert_eq!(edition_from(Some(x2026), ®26, true), "connected", "the edition decision is unchanged");
assert_eq!(registry_years(®26), vec![2026]);
assert!(classic_years_from(Some(x2026), ®26, "connected").is_empty(), "the Connected release is not classic");
assert!(classic_years_from(None, ®26, "connected").is_empty(), "a marker naming no year: no year can be shown to be classic");
let mixed = vec!["SOLIDWORKS 2021".to_string(), "SOLIDWORKS 2026".to_string(), "SOLIDWORKS 3DEXPERIENCE R2026x".to_string()];
assert_eq!(classic_years_from(None, &mixed, "connected"), vec![2021], "a classic 2021 beside a Connected 2026");
assert_eq!(classic_years_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, "desktop"), vec![2021]);
assert_eq!(edition_from(None, &none, false), "unknown");
}
#[test]
fn only_a_positively_identified_desktop_install_may_be_launched() {
let d = |exe: Option<&str>, launcher: bool| Detection {
installed: exe.is_some(),
shortcut: None,
exe: exe.map(String::from),
registry_versions: vec![],
running: false,
launcher_present: launcher,
};
assert!(d(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), false).can_launch());
// Connected: never.
assert!(!d(Some(r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe"), false).can_launch());
// Unknown: never. Refusing a launch we could have made costs one
// sentence; making one we should have refused costs the user a modal
// dialog and a dead process.
assert!(!d(Some(r"D:\weird\SLDWORKS.exe"), false).can_launch());
// Not installed at all: never.
assert!(!d(None, false).can_launch());
// ...and the JSON a caller branches on carries both facts.
let j = d(Some(r"C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\SLDWORKS.exe"), false).to_json();
assert_eq!(j["edition"], "desktop");
assert_eq!(j["canLaunchProgrammatically"], true);
}
#[test]
fn env_expansion_keeps_unknown_vars_literal() {
// No set_var here: mutating the process environment races every other
// test thread (get_var/set_var is not thread-safe), which is UB. Read a
// variable cargo always sets for the test process instead.
let known = "CARGO_PKG_NAME";
let expected = std::env::var(known).unwrap();
assert_eq!(expand_env(&format!("%{known}%/x.exe")), format!("{expected}/x.exe"));
assert_eq!(expand_env(r"%NO_SUCH_VAR_XYZ%\x"), r"%NO_SUCH_VAR_XYZ%\x");
assert_eq!(expand_env("plain"), "plain");
assert_eq!(expand_env("no percent at all"), "no percent at all");
}
#[test]
fn glob_matches_one_wildcard_component() {
let base = std::env::temp_dir().join(format!("swx-glob-{}", std::process::id()));
let deep = base.join("2026").join("SOLIDWORKS");
std::fs::create_dir_all(&deep).unwrap();
let exe = deep.join("SLDWORKS.exe");
std::fs::write(&exe, b"x").unwrap();
let pattern = format!("{}/*/SOLIDWORKS/SLDWORKS.exe", base.display());
let hits = expand_glob(&pattern);
assert_eq!(hits.len(), 1, "{hits:?}");
assert!(hits[0].contains("SLDWORKS.exe"));
// an exact path that does not exist yields nothing
assert!(expand_glob(&format!("{}/nope.exe", base.display())).is_empty());
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn non_windows_detection_is_honest() {
let d = detect();
if !cfg!(windows) {
assert!(!d.installed);
assert_eq!(d.to_json()["platform"], std::env::consts::OS);
}
}
}
#[cfg(test)]
mod live_regression_tests {
use super::expand_glob;
/// A wildcard INSIDE a component (the real 3DEXPERIENCE layout:
/// `SOLIDWORKS 3DEXPERIENCE R2026x`). This is what failed on Drew's live
/// install: the old splitter looked for a dir literally named
/// `SOLIDWORKS 3DEXPERIENCE`.
#[test]
fn partial_component_wildcard_matches_versioned_dir() {
let tmp = std::env::temp_dir().join(format!("swxglob{}", std::process::id()));
let dir = tmp.join("SOLIDWORKS 3DEXPERIENCE R2026x").join("SOLIDWORKS");
std::fs::create_dir_all(&dir).unwrap();
let exe = dir.join("SLDWORKS.exe");
std::fs::write(&exe, b"x").unwrap();
let pat = format!("{}/SOLIDWORKS 3DEXPERIENCE*/SOLIDWORKS/SLDWORKS.exe", tmp.display());
let hits = expand_glob(&pat);
assert_eq!(hits.len(), 1, "expected the versioned dir to match: {hits:?}");
assert!(hits[0].to_lowercase().contains("r2026x"));
// A prefix that does not match must not match anything.
assert!(expand_glob(&format!("{}/NOPE*/SOLIDWORKS/SLDWORKS.exe", tmp.display())).is_empty());
std::fs::remove_dir_all(&tmp).ok();
}
}