dsh-engineering-ui 0.0.0-stage → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +772 -3
- package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
- package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
- package/engineering/plugins/dsh-engineering-ui/lib/client.js +8257 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5634 -0
- package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
- package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
- package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
- package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
- package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
- package/engineering/skills/cad-workflow/SKILL.md +132 -0
- package/engineering/skills/mode-selection/SKILL.md +660 -0
- package/engineering/skills/physics-in-loop/SKILL.md +238 -0
- package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
- package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
- package/engineering/skills/sw-design/SKILL.md +112 -0
- package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
- package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
- package/engineering/skills/sw-to-cad/SKILL.md +121 -0
- package/engineering/skills/webots-simulation/SKILL.md +58 -0
- package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
- package/engineering/tools/_plugin_paths.py +93 -0
- package/engineering/tools/_root.py +330 -0
- package/engineering/tools/_store.py +446 -0
- package/engineering/tools/ac_bridge.py +459 -0
- package/engineering/tools/ac_rules.json +21 -0
- package/engineering/tools/ac_validate.py +699 -0
- package/engineering/tools/ask_user.py +887 -0
- package/engineering/tools/build_arm1.py +46 -0
- package/engineering/tools/build_assembly.py +94 -0
- package/engineering/tools/build_base.py +57 -0
- package/engineering/tools/build_housing.py +70 -0
- package/engineering/tools/build_structural.py +43 -0
- package/engineering/tools/build_transmission.py +69 -0
- package/engineering/tools/cadx.lsp +128 -0
- package/engineering/tools/choice_contract.py +61 -0
- package/engineering/tools/conn_state.py +1671 -0
- package/engineering/tools/defense_gate.py +3374 -0
- package/engineering/tools/drawing_input.py +314 -0
- package/engineering/tools/env_check.py +467 -0
- package/engineering/tools/examples/build_axis.py +142 -0
- package/engineering/tools/examples/build_cup.py +56 -0
- package/engineering/tools/examples/build_plate.py +58 -0
- package/engineering/tools/examples/build_sphere.py +30 -0
- package/engineering/tools/examples/build_transition_duct.py +381 -0
- package/engineering/tools/gate_integrity.py +401 -0
- package/engineering/tools/mode2_flow.py +3589 -0
- package/engineering/tools/mode_checker.py +77 -0
- package/engineering/tools/mode_gate.py +8013 -0
- package/engineering/tools/part_registry.py +4266 -0
- package/engineering/tools/physics/__init__.py +50 -0
- package/engineering/tools/physics/_compat.py +12 -0
- package/engineering/tools/physics/design_state.py +142 -0
- package/engineering/tools/physics/domain_validator.py +751 -0
- package/engineering/tools/physics/fatigue.py +489 -0
- package/engineering/tools/physics/fea_solver.py +1163 -0
- package/engineering/tools/physics/feapy_solver.py +314 -0
- package/engineering/tools/physics/gb_materials.json +1228 -0
- package/engineering/tools/physics/geometry_gate.py +337 -0
- package/engineering/tools/physics/load_case.py +951 -0
- package/engineering/tools/physics/material_db.py +874 -0
- package/engineering/tools/physics/mesh_adapter.py +321 -0
- package/engineering/tools/physics/refine_rules.py +272 -0
- package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
- package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
- package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
- package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
- package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
- package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
- package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
- package/engineering/tools/physics/rules/housing_rules.json +48 -0
- package/engineering/tools/physics/rules/manifest.json +54 -0
- package/engineering/tools/physics/rules/mold_rules.json +48 -0
- package/engineering/tools/physics/rules/structural_rules.json +56 -0
- package/engineering/tools/physics/rules/transmission_rules.json +48 -0
- package/engineering/tools/physics/simulation_report.py +829 -0
- package/engineering/tools/physics_bridge.py +2685 -0
- package/engineering/tools/profile-manifest.ps1 +72 -0
- package/engineering/tools/run_regression.py +109 -0
- package/engineering/tools/settings.py +419 -0
- package/engineering/tools/solidworks-modeling.md +519 -0
- package/engineering/tools/sw_bridge.py +8215 -0
- package/engineering/tools/sw_guard.py +1066 -0
- package/engineering/tools/sw_surface.py +87 -0
- package/engineering/tools/swapi.py +14766 -0
- package/engineering/tools/verify_sync.py +218 -0
- package/engineering/tools/visual_check.py +2589 -0
- package/engineering/tools/webots_env.py +121 -0
- package/engineering/tools/webots_mcp/__init__.py +3 -0
- package/engineering/tools/webots_mcp/__main__.py +3 -0
- package/engineering/tools/webots_mcp/assembly.py +391 -0
- package/engineering/tools/webots_mcp/bootstrap.py +55 -0
- package/engineering/tools/webots_mcp/controller_worker.py +124 -0
- package/engineering/tools/webots_mcp/evidence.py +288 -0
- package/engineering/tools/webots_mcp/fake_worker.py +69 -0
- package/engineering/tools/webots_mcp/fault_resources.py +121 -0
- package/engineering/tools/webots_mcp/host.py +45 -0
- package/engineering/tools/webots_mcp/ledger.py +73 -0
- package/engineering/tools/webots_mcp/owned_job.py +70 -0
- package/engineering/tools/webots_mcp/physical_gate.py +187 -0
- package/engineering/tools/webots_mcp/physics.py +152 -0
- package/engineering/tools/webots_mcp/pipeline.py +108 -0
- package/engineering/tools/webots_mcp/recipes.py +129 -0
- package/engineering/tools/webots_mcp/repair.py +132 -0
- package/engineering/tools/webots_mcp/repair_trial.py +375 -0
- package/engineering/tools/webots_mcp/runtime.py +311 -0
- package/engineering/tools/webots_mcp/server.py +217 -0
- package/engineering/tools/webots_mcp/sw_export.py +509 -0
- package/engineering/tools/webots_mcp/transport.py +156 -0
- package/engineering/tools/workflow_gate.py +6134 -0
- package/package.json +66 -6
|
@@ -0,0 +1,1671 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""conn_state.py — 连接区:SW/CAD 连接的「探测 / 落盘 / 供 AI 判断」单一事实来源
|
|
3
|
+
|
|
4
|
+
═══ 这个模块解决什么问题 ═══════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
用户需求(原文):
|
|
7
|
+
"在设置那边有一个『连接区』,里面有『SW 连接』和『CAD 连接』。
|
|
8
|
+
我想在流程中搞一个代码判断:如果设置那边 SW 连接成功了,
|
|
9
|
+
那么 AI 就直接启动 SW 开始建模;如果没有连接,
|
|
10
|
+
那么就按照之前的代码启动 SW 和 CAD。"
|
|
11
|
+
|
|
12
|
+
还有一个关键诉求:
|
|
13
|
+
"AI 是如何在 Agent 上找 SW 和 CAD 的?如果是直接找其主要的文件地方的话,
|
|
14
|
+
就感觉完全有点多此一举,就可以改为 AI 找对应的地址,
|
|
15
|
+
让 AI 不用再找放在其他盘的时候。"
|
|
16
|
+
|
|
17
|
+
═══ 设计要点(三条) ═══════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
【1】状态落成【JSON 文件】,不放 localStorage。
|
|
20
|
+
为什么:设置页的 UI 状态若只存浏览器 localStorage,**AI/流程侧读不到**,
|
|
21
|
+
"流程中做代码判断"根本无从实现。UI、AI、Python 三方必须读同一个文件。
|
|
22
|
+
位置:`_store.state_dir()/connection_state.json`
|
|
23
|
+
—— 与 mode_state.json / workflow_state.json 同一目录,
|
|
24
|
+
自动跟随 DSH_STATE_DIR / 安装副本优先级,不另造一套约定。
|
|
25
|
+
|
|
26
|
+
【2】把【探测到的路径】一并落盘并交给 AI。
|
|
27
|
+
为什么:`swapi.find_sldworks_exe()` 在注册表查不到时会【全盘符扫描】
|
|
28
|
+
(C/D/E/F/Z…,本机 SW 装在 Z 盘)。这个扫描每次建模前都跑一遍
|
|
29
|
+
纯属重复劳动,而且慢。既然连接区已经探测过了,就把结果缓存下来,
|
|
30
|
+
AI 直接读 `paths.sldworks_exe` / `paths.acad_exe` 用,
|
|
31
|
+
不必再自己去猜"装在哪块盘"。
|
|
32
|
+
|
|
33
|
+
【3】判断是【代码级】的,不依赖模型自觉。
|
|
34
|
+
`is_connected(target)` 返回机器可判定的 True/False;
|
|
35
|
+
`gate(target)` 给出可直接执行的分支结论(skip_startup / next)。
|
|
36
|
+
流程里读这个结论决定"跳过启动排障"还是"走原有启动流程"。
|
|
37
|
+
|
|
38
|
+
═══ 与既有代码的关系(重要 · 不重复造轮子) ═══════════════════════════════
|
|
39
|
+
|
|
40
|
+
· SW 安装路径探测:复用 `swapi.find_sldworks_exe()`(已有注册表+全盘符逻辑)
|
|
41
|
+
· SW 进程探测 :复用 `sw_bridge._sw_processes()`(tasklist + PowerShell 回退)
|
|
42
|
+
· CAD 进程探测 :复用 `ac_bridge._acad_process_running()`
|
|
43
|
+
· CAD 连接 :复用 `ac_bridge.get_ac()`(已内建"未运行则不激活 COM"安全前置)
|
|
44
|
+
· 状态目录 :复用 `_store.state_dir()`(与门禁状态同目录)
|
|
45
|
+
|
|
46
|
+
本模块【只负责】探测编排 + 落盘 + 判断,不重复实现底层探测。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
import json
|
|
52
|
+
import time
|
|
53
|
+
|
|
54
|
+
# 状态文件名(与 mode_state.json / workflow_state.json 并列)
|
|
55
|
+
STATE_NAME = "connection_state.json"
|
|
56
|
+
|
|
57
|
+
# 连接的"新鲜度"上限(秒)。超过则视为过期,需要重新探测。
|
|
58
|
+
# 理由:SW/CAD 进程可能被用户手动关掉;陈旧状态会让流程误判"已连接"
|
|
59
|
+
# 从而跳过必要的启动步骤,最后在建模时才失败(更难排查)。
|
|
60
|
+
STALE_AFTER_SEC = 900 # 15 分钟
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
64
|
+
# 状态文件读写
|
|
65
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
def state_path():
|
|
68
|
+
"""连接状态文件的绝对路径(与门禁状态同目录)。"""
|
|
69
|
+
try:
|
|
70
|
+
import _store
|
|
71
|
+
return _store.state_path(STATE_NAME)
|
|
72
|
+
except Exception:
|
|
73
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
74
|
+
STATE_NAME)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _empty_state():
|
|
78
|
+
return {
|
|
79
|
+
"schema": "dsh-connection-state/1",
|
|
80
|
+
"updated_at": None,
|
|
81
|
+
"updated_at_str": None,
|
|
82
|
+
"sw": {"connected": False, "running": None, "pid": None,
|
|
83
|
+
"processes": [], "checked_at": None, "error": None},
|
|
84
|
+
"cad": {"connected": False, "running": None, "pid": None,
|
|
85
|
+
"processes": [], "checked_at": None, "error": None},
|
|
86
|
+
# 【连接区扩展】Abaqus(无 COM 接口,探测进程 + 安装路径)
|
|
87
|
+
"abaqus": {"connected": False, "running": None, "pid": None,
|
|
88
|
+
"processes": [], "checked_at": None, "error": None,
|
|
89
|
+
"interface": "process-only(Abaqus 无 COM 自动化接口)"},
|
|
90
|
+
# 【连接区扩展】Webots(无 COM 接口,探测进程 + 安装路径)
|
|
91
|
+
"webots": {"connected": False, "running": None, "pid": None,
|
|
92
|
+
"processes": [], "checked_at": None, "error": None,
|
|
93
|
+
"interface": "process-only(Webots 无 COM 自动化接口)"},
|
|
94
|
+
# 【诉求2】探测到的安装路径 —— 交给 AI 直接复用,避免重复全盘扫描
|
|
95
|
+
"paths": {
|
|
96
|
+
"sldworks_exe": None,
|
|
97
|
+
"acad_exe": None,
|
|
98
|
+
"abaqus_exe": None,
|
|
99
|
+
"nx_exe": None,
|
|
100
|
+
"webots_exe": None,
|
|
101
|
+
"scanned_drives": [],
|
|
102
|
+
"resolved_at": None,
|
|
103
|
+
},
|
|
104
|
+
# 【自动启动】最近一次「启动」按钮的执行日志(供 UI 展示 + 一键发给 DSH 诊断)
|
|
105
|
+
"launch": {
|
|
106
|
+
"sw": None,
|
|
107
|
+
"cad": None,
|
|
108
|
+
"abaqus": None,
|
|
109
|
+
"webots": None,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load():
|
|
115
|
+
"""读取连接状态(不存在/损坏时返回空状态,绝不抛异常)。"""
|
|
116
|
+
p = state_path()
|
|
117
|
+
try:
|
|
118
|
+
if os.path.exists(p):
|
|
119
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
120
|
+
d = json.load(f)
|
|
121
|
+
if isinstance(d, dict):
|
|
122
|
+
# 补齐缺字段(兼容将来新增键)
|
|
123
|
+
base = _empty_state()
|
|
124
|
+
for k, v in base.items():
|
|
125
|
+
d.setdefault(k, v)
|
|
126
|
+
if not isinstance(d.get("paths"), dict):
|
|
127
|
+
d["paths"] = base["paths"]
|
|
128
|
+
return d
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return _empty_state()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save(state):
|
|
135
|
+
"""原子写入连接状态(先写临时文件再替换,避免读到半截 JSON)。"""
|
|
136
|
+
p = state_path()
|
|
137
|
+
d = os.path.dirname(p)
|
|
138
|
+
try:
|
|
139
|
+
os.makedirs(d, exist_ok=True)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
state["updated_at"] = time.time()
|
|
143
|
+
state["updated_at_str"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
144
|
+
tmp = p + ".tmp"
|
|
145
|
+
try:
|
|
146
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
147
|
+
json.dump(state, f, ensure_ascii=False, indent=2)
|
|
148
|
+
os.replace(tmp, p)
|
|
149
|
+
return p
|
|
150
|
+
except Exception as e:
|
|
151
|
+
try:
|
|
152
|
+
if os.path.exists(tmp):
|
|
153
|
+
os.remove(tmp)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def age_sec(state=None):
|
|
160
|
+
"""状态距今多少秒;无时间戳返回 None。"""
|
|
161
|
+
st = state or load()
|
|
162
|
+
t = st.get("updated_at")
|
|
163
|
+
if not t:
|
|
164
|
+
return None
|
|
165
|
+
try:
|
|
166
|
+
return max(0.0, time.time() - float(t))
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def is_stale(state=None):
|
|
172
|
+
"""状态是否已过期(超过 STALE_AFTER_SEC 或无时间戳)。"""
|
|
173
|
+
a = age_sec(state)
|
|
174
|
+
return (a is None) or (a > STALE_AFTER_SEC)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
178
|
+
# 路径发现(诉求2:让 AI 不用自己找盘符)
|
|
179
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
180
|
+
|
|
181
|
+
def _find_acad_exe():
|
|
182
|
+
"""探测 AutoCAD 可执行文件路径。
|
|
183
|
+
|
|
184
|
+
搜索顺序(最可靠 → 兜底):
|
|
185
|
+
① 环境变量 DSH_ACAD_EXE(用户显式指定,最高优先)
|
|
186
|
+
② 注册表【递归】搜索 AcadLocation(跨任意安装盘/任意目录)
|
|
187
|
+
③ 运行中进程的可执行路径
|
|
188
|
+
④ 常见安装目录 × 全部盘符
|
|
189
|
+
⑤ 兜底:从注册表拿到的目录直接拼 acad.exe
|
|
190
|
+
|
|
191
|
+
── 【实机事故修复 · 2026-10-04】三个叠加缺陷 ────────────────────────
|
|
192
|
+
实测现象:AutoCAD 2020 明明装着(`Z:\\7-Zip\\AutoCAD 2020\\acad.exe`
|
|
193
|
+
真实存在、5.8MB),连接区却报
|
|
194
|
+
"未找到 acad.exe —— 已扫描系统上全部盘符的常见安装目录",
|
|
195
|
+
即"装没装都找不到",把用户引向"是不是没装"的错误方向。
|
|
196
|
+
|
|
197
|
+
缺陷1(注册表只查一层):
|
|
198
|
+
原实现枚举 `HKLM\\SOFTWARE\\Autodesk\\AutoCAD` 下的【直接子键】
|
|
199
|
+
(R23.1)然后在该层读 AcadLocation —— 但真实的 AcadLocation
|
|
200
|
+
在【第三层】:`...\\AutoCAD\\R23.1\\ACAD-3001:804\\AcadLocation`。
|
|
201
|
+
实测该层只有 `(default)`,读任何值名都 FileNotFoundError。
|
|
202
|
+
⇒ 修法:改为【递归】遍历(深度受限),任何层级出现 GeoLocation
|
|
203
|
+
类值都能命中。
|
|
204
|
+
|
|
205
|
+
缺陷2(只扫 Program Files):
|
|
206
|
+
常见目录只列了 `Program Files\\Autodesk` 等,而本机 AutoCAD 装在
|
|
207
|
+
`Z:\\7-Zip\\AutoCAD 2020` —— 完全不在 Program Files 下。
|
|
208
|
+
⇒ 修法:注册表递归命中优先;全盘扫描时把目录名放宽
|
|
209
|
+
(含 "AutoCAD"/"Autodesk" 关键字的一级目录)。
|
|
210
|
+
|
|
211
|
+
缺陷3(承诺了却没实现的环境变量):
|
|
212
|
+
原错误文案写着"可设置环境变量 DSH_SW_EXE / DSH_ACAD_EXE 显式指定
|
|
213
|
+
路径",但全代码库【没有任何地方读这两个变量】——
|
|
214
|
+
用户照着提示设了也没用。⇒ 修法:本函数与
|
|
215
|
+
`_find_sldworks_exe_env()` 真正读取它们。
|
|
216
|
+
"""
|
|
217
|
+
# ── ⓪ 本机设置 settings.json 的 cad_exe(设置页「工程环境检查」可指定)──
|
|
218
|
+
# 带机器指纹:换电脑自动忽略,不会把别机的盘符带过来。
|
|
219
|
+
try:
|
|
220
|
+
import settings as _settings_mod
|
|
221
|
+
_sv = _settings_mod.cad_exe()
|
|
222
|
+
if _sv and os.path.isfile(_sv):
|
|
223
|
+
return os.path.abspath(_sv)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
# ── ① 环境变量:用户显式指定(最高优先,用于非常规安装)──────────
|
|
227
|
+
_env = (os.environ.get("DSH_ACAD_EXE") or "").strip().strip('"')
|
|
228
|
+
if _env:
|
|
229
|
+
for _c in (_env,
|
|
230
|
+
os.path.join(_env, "acad.exe"),
|
|
231
|
+
os.path.join(_env, "acadlt.exe")):
|
|
232
|
+
try:
|
|
233
|
+
if os.path.isfile(_c):
|
|
234
|
+
return os.path.abspath(_c)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
cands = [] # 注册表收集到的"目录"候选(最后拼 acad.exe 用)
|
|
239
|
+
found = None
|
|
240
|
+
|
|
241
|
+
# ── ② 注册表【递归】搜索(跨任意盘/任意目录,最可靠)───────────────
|
|
242
|
+
if sys.platform == "win32":
|
|
243
|
+
try:
|
|
244
|
+
import winreg
|
|
245
|
+
_VALUE_NAMES = ("AcadLocation", "Location", "InstallDir",
|
|
246
|
+
"InstallLocation", "ProductPath")
|
|
247
|
+
_MAX_DEPTH = 4
|
|
248
|
+
|
|
249
|
+
def _walk(hive, path, depth, out_vals):
|
|
250
|
+
"""递归遍历注册表键,收集含安装路径性质的值。"""
|
|
251
|
+
if depth > _MAX_DEPTH:
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
k = winreg.OpenKey(hive, path)
|
|
255
|
+
except Exception:
|
|
256
|
+
return
|
|
257
|
+
try:
|
|
258
|
+
# 先读本层的候选值
|
|
259
|
+
for vn in _VALUE_NAMES:
|
|
260
|
+
try:
|
|
261
|
+
val, _t = winreg.QueryValueEx(k, vn)
|
|
262
|
+
if isinstance(val, str) and val.strip():
|
|
263
|
+
out_vals.append(val.strip())
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
# 再递归子键
|
|
267
|
+
i = 0
|
|
268
|
+
while True:
|
|
269
|
+
try:
|
|
270
|
+
sub = winreg.EnumKey(k, i)
|
|
271
|
+
i += 1
|
|
272
|
+
except OSError:
|
|
273
|
+
break
|
|
274
|
+
_walk(hive, path + "\\" + sub, depth + 1, out_vals)
|
|
275
|
+
finally:
|
|
276
|
+
try:
|
|
277
|
+
winreg.CloseKey(k)
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
|
|
282
|
+
for base in (r"SOFTWARE\Autodesk\AutoCAD",
|
|
283
|
+
r"SOFTWARE\WOW6432Node\Autodesk\AutoCAD"):
|
|
284
|
+
vals = []
|
|
285
|
+
_walk(hive, base, 0, vals)
|
|
286
|
+
for v in vals:
|
|
287
|
+
# 注册表值可能直接是可执行文件,也可能是目录
|
|
288
|
+
low = v.lower()
|
|
289
|
+
if low.endswith(".exe"):
|
|
290
|
+
try:
|
|
291
|
+
if os.path.isfile(v):
|
|
292
|
+
found = os.path.abspath(v)
|
|
293
|
+
break
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
if v not in cands:
|
|
297
|
+
cands.append(v)
|
|
298
|
+
if found:
|
|
299
|
+
break
|
|
300
|
+
if found:
|
|
301
|
+
break
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
# 先从注册表目录候选里拼 acad.exe(本机就是这条命中)
|
|
306
|
+
if not found:
|
|
307
|
+
for c in cands:
|
|
308
|
+
try:
|
|
309
|
+
for name in ("acad.exe", "Acad.exe", "acadlt.exe", "AcadLT.exe"):
|
|
310
|
+
p = os.path.join(c, name)
|
|
311
|
+
if os.path.isfile(p):
|
|
312
|
+
found = os.path.abspath(p)
|
|
313
|
+
break
|
|
314
|
+
if found:
|
|
315
|
+
break
|
|
316
|
+
except Exception:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# ── ③ 运行中进程反推(最准:它正在跑就说明这个 exe 能用)──────────
|
|
320
|
+
if not found:
|
|
321
|
+
try:
|
|
322
|
+
import subprocess
|
|
323
|
+
out = subprocess.run(
|
|
324
|
+
["powershell", "-NoProfile", "-Command",
|
|
325
|
+
"(Get-Process acad,acadlt -ErrorAction SilentlyContinue | "
|
|
326
|
+
"Select-Object -First 1).Path"],
|
|
327
|
+
capture_output=True, text=True, timeout=20)
|
|
328
|
+
_p = (out.stdout or "").strip()
|
|
329
|
+
if _p and _p.lower().endswith(".exe") and os.path.isfile(_p):
|
|
330
|
+
found = os.path.abspath(_p)
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# ── ④ 常见安装目录 × 全部盘符(兜底)────────────────────────────
|
|
335
|
+
if not found:
|
|
336
|
+
try:
|
|
337
|
+
import swapi
|
|
338
|
+
drives = swapi._all_drive_letters()
|
|
339
|
+
except Exception:
|
|
340
|
+
drives = ["C:"]
|
|
341
|
+
_exe_names = ("acad.exe", "acadlt.exe")
|
|
342
|
+
# 放宽:不只 Program Files,也扫盘符根下含 AutoCAD 关键字的一级目录
|
|
343
|
+
for d in drives:
|
|
344
|
+
bases = [
|
|
345
|
+
os.path.join(d + "\\", "Program Files", "Autodesk"),
|
|
346
|
+
os.path.join(d + "\\", "Program Files (x86)", "Autodesk"),
|
|
347
|
+
os.path.join(d + "\\", "Autodesk"),
|
|
348
|
+
]
|
|
349
|
+
# 盘符根下任意一级目录,名字含 autocad/autodesk 的也纳入
|
|
350
|
+
try:
|
|
351
|
+
for _e in os.listdir(d + "\\"):
|
|
352
|
+
if "autocad" in _e.lower() or "autodesk" in _e.lower():
|
|
353
|
+
bases.append(os.path.join(d + "\\", _e))
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
for base in bases:
|
|
357
|
+
if not os.path.isdir(base):
|
|
358
|
+
continue
|
|
359
|
+
try:
|
|
360
|
+
for root, _dirs, files in os.walk(base):
|
|
361
|
+
for fn in files:
|
|
362
|
+
if fn.lower() in _exe_names:
|
|
363
|
+
found = os.path.abspath(os.path.join(root, fn))
|
|
364
|
+
break
|
|
365
|
+
if found:
|
|
366
|
+
break
|
|
367
|
+
except Exception:
|
|
368
|
+
continue
|
|
369
|
+
if found:
|
|
370
|
+
break
|
|
371
|
+
if found:
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
# ── ⑤ 【连接区加固】有界深度遍历(兜住多层嵌套目录)────────────────
|
|
375
|
+
# 实测缺口:glob/os.walk 白名单只能覆盖 1~2 层,而本机 AutoCAD 装在
|
|
376
|
+
# `Z:\7-Zip\AutoCAD 2020\acad.exe` —— "盘符根 → 7-Zip → 软件目录"
|
|
377
|
+
# 三层嵌套。复用 swapi.walk_drive_for_exe()(与 SW 侧同一实现)。
|
|
378
|
+
if not found:
|
|
379
|
+
try:
|
|
380
|
+
import swapi
|
|
381
|
+
for d in swapi._all_drive_letters():
|
|
382
|
+
_hit = swapi.walk_drive_for_exe(
|
|
383
|
+
d, ("acad.exe", "acadlt.exe"),
|
|
384
|
+
keyword="autocad", max_depth=3)
|
|
385
|
+
if not _hit:
|
|
386
|
+
# 有的装法目录名是 Autodesk\AutoCAD 20xx,关键字再放宽一档
|
|
387
|
+
_hit = swapi.walk_drive_for_exe(
|
|
388
|
+
d, ("acad.exe", "acadlt.exe"),
|
|
389
|
+
keyword="autodesk", max_depth=3)
|
|
390
|
+
if _hit:
|
|
391
|
+
found = _hit
|
|
392
|
+
break
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
return found
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def discover_paths(force=False):
|
|
399
|
+
"""探测并返回 SW / CAD / Abaqus / NX 安装路径。
|
|
400
|
+
|
|
401
|
+
【诉求2】把结果落盘,AI 直接读,不必重复"猜装在哪块盘"。
|
|
402
|
+
|
|
403
|
+
缓存策略:已解析过且未 force 时,直接复用文件里的结果 ——
|
|
404
|
+
除非该路径已不存在(例如软件被卸载/换盘),那时才重新扫描。
|
|
405
|
+
|
|
406
|
+
── 【换机事故修复】缓存必须绑定【本机指纹】────────────────────────────
|
|
407
|
+
实测缺陷:connection_state.json 会随仓库/安装副本一起被拷到另一台电脑,
|
|
408
|
+
里面记着原作者机器的 Z 盘路径(Program Files 下的 SOLIDWORKS)。
|
|
409
|
+
旧实现只要任意一个已解析路径仍存在就沿用整份缓存,于是那台电脑
|
|
410
|
+
永远拿不到自己的正确路径,每次连接都先撞一次不存在的 Z 盘。
|
|
411
|
+
现在:paths 里多存一个 machine(本机指纹);指纹不符 = 整份缓存作废,
|
|
412
|
+
按本机重新探测,并把旧值留在 ignored_stale_paths 里备查。
|
|
413
|
+
"""
|
|
414
|
+
st = load()
|
|
415
|
+
paths = st.get("paths") or {}
|
|
416
|
+
# ── 跨机缓存作废 ───────────────────────────────────────────────────
|
|
417
|
+
try:
|
|
418
|
+
import settings as _settings_mod
|
|
419
|
+
_mine = _settings_mod.machine_fingerprint()
|
|
420
|
+
_owner = paths.get("machine")
|
|
421
|
+
if _owner and _owner != _mine:
|
|
422
|
+
_stale = {k: paths.get(k) for k in
|
|
423
|
+
("sldworks_exe", "acad_exe", "abaqus_exe", "nx_exe", "webots_exe")
|
|
424
|
+
if paths.get(k)}
|
|
425
|
+
paths = {"ignored_stale_paths": _stale,
|
|
426
|
+
"ignored_reason": ("这份路径记录来自另一台电脑"
|
|
427
|
+
"(指纹 %s,本机 %s)" % (_owner, _mine))}
|
|
428
|
+
force = True
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
sw_exe = paths.get("sldworks_exe")
|
|
432
|
+
acad_exe = paths.get("acad_exe")
|
|
433
|
+
abq_exe = paths.get("abaqus_exe")
|
|
434
|
+
ans_exe = paths.get("nx_exe")
|
|
435
|
+
web_exe = paths.get("webots_exe")
|
|
436
|
+
cached_ok = (not force) and (sw_exe or acad_exe or abq_exe or ans_exe or web_exe)
|
|
437
|
+
if cached_ok:
|
|
438
|
+
# 缓存有效性:至少有一个已解析路径仍真实存在
|
|
439
|
+
if any(p and os.path.exists(p)
|
|
440
|
+
for p in (sw_exe, acad_exe, abq_exe, ans_exe, web_exe)):
|
|
441
|
+
# ── 【连接区扩展·升级兼容】旧状态文件里没有新增目标的键 ──────
|
|
442
|
+
# 直接 return 会让 webots_exe / abaqus_exe 永远缺失(UI 显示的
|
|
443
|
+
# "已连接"与 paths 里查不到 exe 自相矛盾)。这里只补缺的键,
|
|
444
|
+
# 不重扫 SW/CAD(那才是慢的部分)。
|
|
445
|
+
_missing = [k for k in ("abaqus_exe", "webots_exe")
|
|
446
|
+
if k not in paths]
|
|
447
|
+
if not _missing:
|
|
448
|
+
return paths
|
|
449
|
+
getBackfill = _find_cae_exe
|
|
450
|
+
idx2 = {"abaqus_exe": "abaqus", "webots_exe": "webots"}
|
|
451
|
+
for _k in _missing:
|
|
452
|
+
try:
|
|
453
|
+
paths[_k] = getBackfill(_CAE_TARGETS[idx2[_k]])
|
|
454
|
+
except Exception:
|
|
455
|
+
paths[_k] = None
|
|
456
|
+
st["paths"] = paths
|
|
457
|
+
save(st)
|
|
458
|
+
return paths
|
|
459
|
+
# 重新探测
|
|
460
|
+
_sw = None
|
|
461
|
+
try:
|
|
462
|
+
import swapi
|
|
463
|
+
_sw = swapi.find_sldworks_exe()
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
_ac = _find_acad_exe()
|
|
467
|
+
# ── 【连接区扩展】CAE 安装路径(Abaqus / NX)──────────────────────
|
|
468
|
+
# 与 SW/CAD 同一套"跨盘符 × 常见安装目录"思路;找不到就是 None,
|
|
469
|
+
# 不影响其它目标。
|
|
470
|
+
_cae_exe = {}
|
|
471
|
+
for _t, _cfg in _CAE_TARGETS.items():
|
|
472
|
+
try:
|
|
473
|
+
_cae_exe[_t] = _find_cae_exe(_cfg)
|
|
474
|
+
except Exception:
|
|
475
|
+
_cae_exe[_t] = None
|
|
476
|
+
try:
|
|
477
|
+
import swapi
|
|
478
|
+
_drives = swapi._all_drive_letters()
|
|
479
|
+
except Exception:
|
|
480
|
+
_drives = []
|
|
481
|
+
_prev_ignored = (paths or {}).get("ignored_stale_paths")
|
|
482
|
+
paths = {
|
|
483
|
+
"sldworks_exe": _sw,
|
|
484
|
+
"acad_exe": _ac,
|
|
485
|
+
"abaqus_exe": _cae_exe.get("abaqus"),
|
|
486
|
+
"nx_exe": _cae_exe.get("nx"),
|
|
487
|
+
"webots_exe": _cae_exe.get("webots"),
|
|
488
|
+
"scanned_drives": _drives,
|
|
489
|
+
"resolved_at": time.time(),
|
|
490
|
+
"resolved_at_str": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
491
|
+
# ★ 本机指纹:换电脑后整份路径缓存作废(见函数开头)
|
|
492
|
+
"machine": _mine,
|
|
493
|
+
"host": (os.environ.get("COMPUTERNAME") or ""),
|
|
494
|
+
}
|
|
495
|
+
if _prev_ignored:
|
|
496
|
+
paths["ignored_stale_paths"] = _prev_ignored
|
|
497
|
+
st["paths"] = paths
|
|
498
|
+
save(st)
|
|
499
|
+
return paths
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
503
|
+
# 探测(进程层 + COM 层)
|
|
504
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
505
|
+
|
|
506
|
+
def _sw_process_list():
|
|
507
|
+
"""枚举 SLDWORKS.EXE 进程(tasklist,零副作用)。
|
|
508
|
+
|
|
509
|
+
⚠️ 【为什么不 import sw_bridge】────────────────────────────────────────
|
|
510
|
+
本模块若 `import sw_bridge` 会形成【循环导入】:
|
|
511
|
+
sw_bridge → conn_state(cmd_conn_probe 里 import)→ sw_bridge
|
|
512
|
+
更危险的是:sw_bridge 作为 `__main__` 运行时,Python 会把它
|
|
513
|
+
【再导入一次】成一个独立的模块对象,而该对象可能解析到
|
|
514
|
+
另一份副本(安装副本)→ 拿到的是**旧版本**,属性缺失。
|
|
515
|
+
实测现象:`AttributeError: module 'sw_bridge' has no attribute
|
|
516
|
+
'_sw_processes'`(明明源码里有)。
|
|
517
|
+
因此:进程探测在本模块【自带实现】,只依赖标准库。
|
|
518
|
+
COM 连接才去 import swapi / ac_bridge(它们不反向依赖本模块)。
|
|
519
|
+
"""
|
|
520
|
+
import subprocess
|
|
521
|
+
procs = []
|
|
522
|
+
err = None
|
|
523
|
+
try:
|
|
524
|
+
o = subprocess.run(
|
|
525
|
+
["tasklist", "/FI", "IMAGENAME eq SLDWORKS.EXE",
|
|
526
|
+
"/FO", "CSV", "/NH"],
|
|
527
|
+
capture_output=True, text=True, timeout=15)
|
|
528
|
+
txt = (o.stdout or "").strip()
|
|
529
|
+
if "SLDWORKS.EXE" in txt.upper():
|
|
530
|
+
import csv
|
|
531
|
+
import io
|
|
532
|
+
for row in csv.reader(io.StringIO(txt)):
|
|
533
|
+
if not row:
|
|
534
|
+
continue
|
|
535
|
+
nm = (row[0] or "").strip().strip('"')
|
|
536
|
+
if nm.upper() != "SLDWORKS.EXE":
|
|
537
|
+
continue
|
|
538
|
+
try:
|
|
539
|
+
pid = int(str(row[1]).strip().strip('"'))
|
|
540
|
+
except (ValueError, IndexError):
|
|
541
|
+
pid = None
|
|
542
|
+
procs.append({
|
|
543
|
+
"pid": pid, "name": nm,
|
|
544
|
+
"mem": (row[4].strip().strip('"')
|
|
545
|
+
if len(row) > 4 else None),
|
|
546
|
+
})
|
|
547
|
+
return procs, None
|
|
548
|
+
except Exception as e:
|
|
549
|
+
err = "tasklist 失败: %r" % (e,)
|
|
550
|
+
# PowerShell 回退
|
|
551
|
+
try:
|
|
552
|
+
import subprocess
|
|
553
|
+
o = subprocess.run(
|
|
554
|
+
["powershell", "-NoProfile", "-Command",
|
|
555
|
+
"Get-Process sldworks -ErrorAction SilentlyContinue | "
|
|
556
|
+
"Select-Object Id,ProcessName | ConvertTo-Json -Compress"],
|
|
557
|
+
capture_output=True, text=True, timeout=20)
|
|
558
|
+
txt = (o.stdout or "").strip()
|
|
559
|
+
if txt:
|
|
560
|
+
data = json.loads(txt)
|
|
561
|
+
if isinstance(data, dict):
|
|
562
|
+
data = [data]
|
|
563
|
+
for d in (data or []):
|
|
564
|
+
procs.append({"pid": d.get("Id"),
|
|
565
|
+
"name": d.get("ProcessName"), "mem": None})
|
|
566
|
+
return procs, None
|
|
567
|
+
except Exception as e2:
|
|
568
|
+
err = "%s; powershell 回退也失败: %r" % (err or "", e2)
|
|
569
|
+
return [], err
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def probe_sw(with_com=True):
|
|
573
|
+
"""探测 SolidWorks:进程层 + (可选)COM 连接层。
|
|
574
|
+
|
|
575
|
+
返回 dict:{running, pid, processes, connected, error}
|
|
576
|
+
|
|
577
|
+
⚠️ 【重要】with_com=False 时【只做进程探测】,一个 COM 调用都不发 ——
|
|
578
|
+
避免"只想看看开没开"却把 SolidWorks 拉起来。
|
|
579
|
+
"""
|
|
580
|
+
out = {"running": None, "pid": None, "processes": [],
|
|
581
|
+
"connected": False, "error": None}
|
|
582
|
+
# ① 进程层(零副作用,自带实现,不 import sw_bridge —— 见函数注释)
|
|
583
|
+
procs, err = _sw_process_list()
|
|
584
|
+
out["processes"] = procs or []
|
|
585
|
+
out["running"] = bool(procs)
|
|
586
|
+
if procs:
|
|
587
|
+
out["pid"] = procs[0].get("pid")
|
|
588
|
+
if err:
|
|
589
|
+
out["error"] = err
|
|
590
|
+
out["running"] = None
|
|
591
|
+
# ② COM 层:只有"已在运行"时才去连(未运行绝不 Dispatch,避免拉起 SW)
|
|
592
|
+
if with_com and out.get("running"):
|
|
593
|
+
try:
|
|
594
|
+
import swapi
|
|
595
|
+
sw = swapi.get_sw()
|
|
596
|
+
if sw is not None:
|
|
597
|
+
_ = sw.RevisionNumber # 真实读取以确认连接
|
|
598
|
+
out["connected"] = True
|
|
599
|
+
out["revision"] = str(sw.RevisionNumber)
|
|
600
|
+
out["binding"] = getattr(sw, "_dsh_binding", None)
|
|
601
|
+
try:
|
|
602
|
+
out["pid"] = out["pid"] or sw.GetProcessID
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
605
|
+
except Exception as e:
|
|
606
|
+
out["connected"] = False
|
|
607
|
+
out["error"] = "COM 连接失败: %r" % (e,)
|
|
608
|
+
elif not out.get("running"):
|
|
609
|
+
out["connected"] = False
|
|
610
|
+
return out
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def probe_cad(with_com=True):
|
|
614
|
+
"""探测 AutoCAD:进程层 + (可选)COM 连接层。
|
|
615
|
+
|
|
616
|
+
⚠️ 复用 ac_bridge 的【安全前置检查】:AutoCAD 未运行时
|
|
617
|
+
`Dispatch('AutoCAD.Application')` 会触发 COM 激活去【启动】CAD,
|
|
618
|
+
既慢又可能在安装/许可异常时把 CAD 弄崩(已实测事故)。
|
|
619
|
+
因此未运行就只报"未运行",绝不尝试连接。
|
|
620
|
+
"""
|
|
621
|
+
out = {"running": None, "pid": None, "processes": [],
|
|
622
|
+
"connected": False, "error": None}
|
|
623
|
+
# ① 进程层(零副作用)
|
|
624
|
+
try:
|
|
625
|
+
import ac_bridge
|
|
626
|
+
# ⚠️ 用 getattr 兜底:若 ac_bridge 被解析到【旧副本】(不含本函数),
|
|
627
|
+
# 不能让整个探测崩掉 —— 回退到自带的 tasklist 探测。
|
|
628
|
+
_fn = getattr(ac_bridge, "_acad_process_running", None)
|
|
629
|
+
if callable(_fn):
|
|
630
|
+
running = _fn()
|
|
631
|
+
else:
|
|
632
|
+
running = bool(_acad_process_list())
|
|
633
|
+
out["running"] = bool(running)
|
|
634
|
+
if running:
|
|
635
|
+
out["processes"] = _acad_process_list()
|
|
636
|
+
if out["processes"]:
|
|
637
|
+
out["pid"] = out["processes"][0].get("pid")
|
|
638
|
+
except Exception as e:
|
|
639
|
+
# ac_bridge 不可用 → 直接用自带进程探测(不阻断)
|
|
640
|
+
try:
|
|
641
|
+
lst = _acad_process_list()
|
|
642
|
+
out["running"] = bool(lst)
|
|
643
|
+
out["processes"] = lst
|
|
644
|
+
if lst:
|
|
645
|
+
out["pid"] = lst[0].get("pid")
|
|
646
|
+
except Exception:
|
|
647
|
+
out["error"] = "进程探测失败: %r" % (e,)
|
|
648
|
+
# ② COM 层:仅当已在运行
|
|
649
|
+
if with_com and out.get("running"):
|
|
650
|
+
ac = None
|
|
651
|
+
try:
|
|
652
|
+
import ac_bridge
|
|
653
|
+
ac = ac_bridge.get_ac()
|
|
654
|
+
if ac:
|
|
655
|
+
st = ac.status()
|
|
656
|
+
out["connected"] = bool(st.get("connected"))
|
|
657
|
+
out["drawing"] = st.get("drawing")
|
|
658
|
+
except Exception as e:
|
|
659
|
+
out["connected"] = False
|
|
660
|
+
out["error"] = "COM 连接失败: %r" % (e,)
|
|
661
|
+
finally:
|
|
662
|
+
try:
|
|
663
|
+
if ac is not None:
|
|
664
|
+
ac.close()
|
|
665
|
+
except Exception:
|
|
666
|
+
pass
|
|
667
|
+
return out
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _acad_process_list():
|
|
671
|
+
"""枚举 acad.exe 进程(tasklist)。"""
|
|
672
|
+
import subprocess
|
|
673
|
+
procs = []
|
|
674
|
+
try:
|
|
675
|
+
o = subprocess.run(
|
|
676
|
+
["tasklist", "/FI", "IMAGENAME eq acad.exe", "/FO", "CSV", "/NH"],
|
|
677
|
+
capture_output=True, text=True, timeout=15)
|
|
678
|
+
txt = (o.stdout or "").strip()
|
|
679
|
+
if "acad.exe" in txt.lower():
|
|
680
|
+
import csv
|
|
681
|
+
import io
|
|
682
|
+
for row in csv.reader(io.StringIO(txt)):
|
|
683
|
+
if not row:
|
|
684
|
+
continue
|
|
685
|
+
nm = (row[0] or "").strip().strip('"')
|
|
686
|
+
if nm.lower() != "acad.exe":
|
|
687
|
+
continue
|
|
688
|
+
try:
|
|
689
|
+
pid = int(str(row[1]).strip().strip('"'))
|
|
690
|
+
except (ValueError, IndexError):
|
|
691
|
+
pid = None
|
|
692
|
+
procs.append({"pid": pid, "name": nm})
|
|
693
|
+
except Exception:
|
|
694
|
+
pass
|
|
695
|
+
return procs
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def probe_all(with_com=True, resolve_paths=True):
|
|
699
|
+
"""完整探测 SW + CAD + Abaqus + NX(+ 路径),并落盘。返回最新状态 dict。"""
|
|
700
|
+
st = load()
|
|
701
|
+
now = time.time()
|
|
702
|
+
now_str = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
703
|
+
sw = probe_sw(with_com=with_com)
|
|
704
|
+
sw["checked_at"] = now_str
|
|
705
|
+
st["sw"] = sw
|
|
706
|
+
cad = probe_cad(with_com=with_com)
|
|
707
|
+
cad["checked_at"] = now_str
|
|
708
|
+
st["cad"] = cad
|
|
709
|
+
# 【NX → Webots 改造】webots 与 abaqus 同属"进程 + 安装路径"型目标,
|
|
710
|
+
# 一并纳入全量探测(旧名 nx 仍保留探测,状态文件向后兼容)。
|
|
711
|
+
for _t in ("abaqus", "webots", "nx"):
|
|
712
|
+
_cae = probe_cae(_t)
|
|
713
|
+
_cae["checked_at"] = now_str
|
|
714
|
+
st[_t] = _cae
|
|
715
|
+
if resolve_paths:
|
|
716
|
+
st["paths"] = discover_paths()
|
|
717
|
+
save(st)
|
|
718
|
+
return st
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _abaqus_process_list():
|
|
722
|
+
"""列出 Abaqus 相关进程(零副作用,只读 tasklist)。
|
|
723
|
+
|
|
724
|
+
Abaqus 的进程名因版本/组件而异,常见有:
|
|
725
|
+
· ABQcaeK.exe / abaqus.exe / ABQcaeG.exe(CAE 图形界面)
|
|
726
|
+
· standard.exe / explicit.exe(求解器)
|
|
727
|
+
· ABQLauncher.exe / abqlauncher.exe(启动器)
|
|
728
|
+
"""
|
|
729
|
+
return _tasklist_match(("ABQCAEK", "ABQCAEG", "ABAQUS", "ABQLAUNCHER",
|
|
730
|
+
"STANDARD", "EXPLICIT", "ABQSMA"))
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _nx_process_list():
|
|
734
|
+
"""列出 Siemens NX 相关进程(零副作用,只读 tasklist)。
|
|
735
|
+
|
|
736
|
+
常见进程名:
|
|
737
|
+
· ugraf.exe —— NX 的图形主进程(最典型)
|
|
738
|
+
· nx.exe / ug_ii.exe —— 启动器/交互
|
|
739
|
+
· NXNASTRAN.exe —— NX Nastran 求解器
|
|
740
|
+
· ugslmd.exe —— NX 许可证服务(License Daemon)
|
|
741
|
+
"""
|
|
742
|
+
return _tasklist_match(("UGRAF", "NXNASTRAN", "UGSLMD",
|
|
743
|
+
"UG_II", "NX.EXE"))
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _webots_process_list():
|
|
747
|
+
"""列出 Webots 相关进程(零副作用,只读 tasklist)。
|
|
748
|
+
|
|
749
|
+
Webots 的进程名:
|
|
750
|
+
· webots.exe —— 主程序(Windows 版位于 msys64/mingw64/bin/)
|
|
751
|
+
· webots-controller.exe —— 控制器(TCP 控制器协议)
|
|
752
|
+
· webots-launcher.exe —— 新版启动器
|
|
753
|
+
"""
|
|
754
|
+
return _tasklist_match(("WEBOTS",))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _tasklist_match(keywords):
|
|
758
|
+
"""通用 tasklist 关键词匹配(返回 [{name, pid}])。
|
|
759
|
+
|
|
760
|
+
抽出来给 Abaqus / NX 共用 —— 两者的探测方式完全相同(都是"按进程名
|
|
761
|
+
关键词匹配"),只有关键词不同,没必要写两份重复代码。
|
|
762
|
+
"""
|
|
763
|
+
found = []
|
|
764
|
+
try:
|
|
765
|
+
import subprocess
|
|
766
|
+
out = subprocess.run(
|
|
767
|
+
["tasklist", "/FO", "CSV", "/NH"],
|
|
768
|
+
capture_output=True, text=True, timeout=20,
|
|
769
|
+
creationflags=(0x08000000 if sys.platform == "win32" else 0))
|
|
770
|
+
for line in (out.stdout or "").splitlines():
|
|
771
|
+
_l = line.strip()
|
|
772
|
+
if not _l.startswith('"'):
|
|
773
|
+
continue
|
|
774
|
+
try:
|
|
775
|
+
_parts = [x.strip('"') for x in _l.split('","')]
|
|
776
|
+
_nm = _parts[0]
|
|
777
|
+
_pid = int(_parts[1])
|
|
778
|
+
except Exception:
|
|
779
|
+
continue
|
|
780
|
+
_up = _nm.upper()
|
|
781
|
+
if any(_k in _up for _k in keywords):
|
|
782
|
+
found.append({"name": _nm, "pid": _pid})
|
|
783
|
+
except Exception:
|
|
784
|
+
pass
|
|
785
|
+
return found
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
# ── 通用 CAE 目标描述表(Abaqus / NX)────────────────────────────────
|
|
789
|
+
# 两者的能力边界相同:都【没有 COM 自动化接口】,集成方式是命令行/脚本调用
|
|
790
|
+
# (abaqus python / run_journal)。因此共用一套探测逻辑,只在
|
|
791
|
+
# "安装目录关键词 / 可执行文件名 / 显示名 / 集成方式" 上区分。
|
|
792
|
+
_CAE_TARGETS = {
|
|
793
|
+
"abaqus": {
|
|
794
|
+
"display": "Abaqus",
|
|
795
|
+
"install_dirs": ("SIMULIA", "Abaqus", "Dassault Systemes",
|
|
796
|
+
"Program Files\\SIMULIA",
|
|
797
|
+
"Program Files\\Dassault Systemes",
|
|
798
|
+
"Program Files (x86)\\SIMULIA"),
|
|
799
|
+
"exe_names": ("abaqus.bat", "abq*.exe", "ABQcaeK.exe", "ABQcaeG.exe",
|
|
800
|
+
"abaqus.exe", "abqlauncher.exe"),
|
|
801
|
+
"process_fn": _abaqus_process_list,
|
|
802
|
+
"integration": ("`abaqus python <脚本>` 或 `abaqus cae noGUI=<脚本>`"),
|
|
803
|
+
"version_re": r"(20\d{2}|6\.\d{1,2})",
|
|
804
|
+
},
|
|
805
|
+
"nx": {
|
|
806
|
+
"display": "NX",
|
|
807
|
+
# Siemens NX 的安装根常见为 <盘>:\Program Files\Siemens\NX<版本>
|
|
808
|
+
"install_dirs": ("Siemens", "Siemens NX", "NX",
|
|
809
|
+
"Program Files\\Siemens",
|
|
810
|
+
"Program Files\\Siemens NX",
|
|
811
|
+
"Program Files (x86)\\Siemens"),
|
|
812
|
+
"exe_names": ("ugraf.exe", "nx.exe", "ug_ii.exe", "run_journal.exe",
|
|
813
|
+
"NXNASTRAN.exe"),
|
|
814
|
+
"process_fn": _nx_process_list,
|
|
815
|
+
"integration": ("`run_journal <脚本.py>`(NX Open 批处理)"
|
|
816
|
+
" 或 NX Nastran 求解器命令行"),
|
|
817
|
+
"version_re": r"(NX\s*\d+(?:\.\d+)?|1[89]\.\d|2[0-9]\.\d|20\d{2})",
|
|
818
|
+
},
|
|
819
|
+
# ── 【NX → Webots 改造】Webots 也是"进程 + 安装路径"型目标 ──────────
|
|
820
|
+
# 与 Abaqus / NX 完全同一套探测逻辑(无 COM,走 webots-controller TCP)。
|
|
821
|
+
# 安装布局(Windows):<盘>:\Webots\msys64\mingw64\bin\webots.exe,
|
|
822
|
+
# 且安装器通常会写环境变量 WEBOTS_HOME(例:D:\Webots)—— 见 _find_cae_exe。
|
|
823
|
+
"webots": {
|
|
824
|
+
"display": "Webots",
|
|
825
|
+
"install_dirs": ("Webots", "Cyberbotics", "Program Files\\Webots",
|
|
826
|
+
"Program Files\\Cyberbotics",
|
|
827
|
+
"Program Files (x86)\\Webots"),
|
|
828
|
+
"exe_names": ("webots.exe",),
|
|
829
|
+
"process_fn": _webots_process_list,
|
|
830
|
+
"integration": ("`webots --mode=fast <世界文件.wbt>` 启动仿真,"
|
|
831
|
+
"外部控制走 webots-controller(TCP 控制器协议)"),
|
|
832
|
+
"version_re": r"(R\d{4}[a-z]?|20\d{2})",
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def probe_cae(target):
|
|
838
|
+
"""探测 CAE 软件(Abaqus / NX):进程层 + 安装路径。
|
|
839
|
+
|
|
840
|
+
── 设计取舍:不做 COM 连接 ────────────────────────────────────────────
|
|
841
|
+
Abaqus 与 NX 都**没有可用的 COM 自动化接口**(它们的脚本接口是各自的
|
|
842
|
+
Python/APDL,需在其进程内或命令行下运行)。因此这里【只做进程与安装
|
|
843
|
+
探测】,不尝试任何"建立会话"的动作:
|
|
844
|
+
· 进程在跑 → running=True;
|
|
845
|
+
· 能找到安装路径 → 记录 exe(供后续命令行调用);
|
|
846
|
+
· connected 语义 = "检测到可用安装或进程",
|
|
847
|
+
而非"已建立 COM 会话",避免给出误导性的"已连接"。
|
|
848
|
+
这与 SW/CAD 的按钮外观完全一致,但结论口径如实反映能力边界
|
|
849
|
+
(返回体里带 interface 字段说明)。
|
|
850
|
+
"""
|
|
851
|
+
_cfg = _CAE_TARGETS.get(str(target or "").lower())
|
|
852
|
+
_disp = (_cfg or {}).get("display", str(target))
|
|
853
|
+
out = {"running": None, "pid": None, "processes": [],
|
|
854
|
+
"connected": False, "error": None,
|
|
855
|
+
"interface": "process-only(%s 无 COM 自动化接口)" % _disp,
|
|
856
|
+
"integration": (_cfg or {}).get("integration")}
|
|
857
|
+
if not _cfg:
|
|
858
|
+
out["error"] = "未知 CAE 目标: %r" % (target,)
|
|
859
|
+
return out
|
|
860
|
+
# ① 进程层(零副作用)
|
|
861
|
+
try:
|
|
862
|
+
lst = (_cfg.get("process_fn") or (lambda: []))()
|
|
863
|
+
out["running"] = bool(lst)
|
|
864
|
+
out["processes"] = lst
|
|
865
|
+
if lst:
|
|
866
|
+
out["pid"] = lst[0].get("pid")
|
|
867
|
+
except Exception as e:
|
|
868
|
+
out["error"] = "进程探测失败: %r" % (e,)
|
|
869
|
+
# ② 安装路径(跨盘符;有安装即视为"可用",命令行可调用)
|
|
870
|
+
try:
|
|
871
|
+
exe = _find_cae_exe(_cfg)
|
|
872
|
+
if exe:
|
|
873
|
+
out["exe"] = exe
|
|
874
|
+
out["revision"] = _cae_version_from_path(exe, _cfg.get("version_re"))
|
|
875
|
+
out["connected"] = True
|
|
876
|
+
out["note"] = ("检测到 %s 安装:%s。%s 无 COM 接口,集成方式为 %s。"
|
|
877
|
+
% (_disp, exe, _disp, _cfg.get("integration") or "命令行调用"))
|
|
878
|
+
elif out.get("running"):
|
|
879
|
+
out["connected"] = True
|
|
880
|
+
out["note"] = "检测到 %s 进程在运行(未定位到安装路径)" % _disp
|
|
881
|
+
else:
|
|
882
|
+
out["note"] = "未检测到 %s 进程或安装" % _disp
|
|
883
|
+
except Exception as e:
|
|
884
|
+
out["error"] = (out.get("error") or "") + " 路径探测失败: %r" % (e,)
|
|
885
|
+
return out
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _find_cae_exe(cfg):
|
|
889
|
+
"""跨盘符查找 CAE 可执行文件(按 cfg 的 install_dirs × exe_names)。
|
|
890
|
+
|
|
891
|
+
只做有限深度扫描(在已知安装目录下 glob),避免全盘遍历拖慢探测。
|
|
892
|
+
"""
|
|
893
|
+
import glob as _glob
|
|
894
|
+
pats = []
|
|
895
|
+
try:
|
|
896
|
+
roots = []
|
|
897
|
+
# 复用 SW 那套"枚举真实盘符"的思路(A~Z 中存在的盘)
|
|
898
|
+
try:
|
|
899
|
+
import swapi as _sw
|
|
900
|
+
_dr = getattr(_sw, "_drive_roots", None)
|
|
901
|
+
if callable(_dr):
|
|
902
|
+
roots = list(_dr())
|
|
903
|
+
except Exception:
|
|
904
|
+
roots = []
|
|
905
|
+
if not roots:
|
|
906
|
+
import string
|
|
907
|
+
roots = ["%s:\\" % c for c in string.ascii_uppercase
|
|
908
|
+
if os.path.isdir("%s:\\" % c)]
|
|
909
|
+
for _d in roots:
|
|
910
|
+
for _sub in (cfg.get("install_dirs") or ()):
|
|
911
|
+
pats.append(os.path.join(_d, _sub))
|
|
912
|
+
except Exception:
|
|
913
|
+
return None
|
|
914
|
+
# ── 【NX → Webots 改造】Webots 安装器会写 WEBOTS_HOME(如 D:\Webots),
|
|
915
|
+
# 直接用它做第一候选,比按 Program Files 猜路径可靠得多。
|
|
916
|
+
if (cfg.get("display") or "").lower() == "webots":
|
|
917
|
+
_wh = (os.environ.get("WEBOTS_HOME") or "").strip()
|
|
918
|
+
if _wh and os.path.isdir(_wh):
|
|
919
|
+
pats.insert(0, _wh)
|
|
920
|
+
for _base in pats:
|
|
921
|
+
if not os.path.isdir(_base):
|
|
922
|
+
continue
|
|
923
|
+
for _nm in (cfg.get("exe_names") or ()):
|
|
924
|
+
try:
|
|
925
|
+
for _hit in _glob.glob(os.path.join(_base, "**", _nm),
|
|
926
|
+
recursive=True)[:1]:
|
|
927
|
+
if os.path.isfile(_hit):
|
|
928
|
+
return _hit
|
|
929
|
+
except Exception:
|
|
930
|
+
continue
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _cae_version_from_path(p, ver_re=None):
|
|
935
|
+
"""从路径里解析版本号(如 ...\\SIMULIA\\Abaqus\\2023\\... → Abaqus 2023)。"""
|
|
936
|
+
import re as _re
|
|
937
|
+
try:
|
|
938
|
+
m = _re.search(ver_re or r"(20\d{2})", str(p or ""))
|
|
939
|
+
return m.group(1) if m else None
|
|
940
|
+
except Exception:
|
|
941
|
+
return None
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def probe_abaqus(with_com=True):
|
|
945
|
+
"""探测 Abaqus(保留旧签名,内部转调通用 probe_cae)。"""
|
|
946
|
+
return probe_cae("abaqus")
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def probe_nx(with_com=True):
|
|
950
|
+
"""探测 NX(内部转调通用 probe_cae)。"""
|
|
951
|
+
return probe_cae("nx")
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def probe_target(target, with_com=True):
|
|
955
|
+
"""只探测单个目标('sw' | 'cad' | 'abaqus' | 'nx' | 占位项),其余字段保持原值,落盘。
|
|
956
|
+
|
|
957
|
+
【连接区扩展】新增 'abaqus' / 'nx' —— 两者都无 COM 接口,
|
|
958
|
+
只做进程/安装探测(见 probe_cae 的设计说明)。
|
|
959
|
+
占位项('placeholder*')不探测、不报错:设置页里它们是未启用的
|
|
960
|
+
预留位,探测一个不存在的目标只会制造噪音;这里明确返回
|
|
961
|
+
"占位项,未接入探测"的结论,保证 UI 行为可预期。
|
|
962
|
+
"""
|
|
963
|
+
st = load()
|
|
964
|
+
now_str = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
965
|
+
t = str(target or "").lower()
|
|
966
|
+
# 【NX → Webots 改造】旧名先归一,保证写入的是规范键(webots)
|
|
967
|
+
t = _TARGET_ALIASES.get(t, t)
|
|
968
|
+
if t == "sw":
|
|
969
|
+
sw = probe_sw(with_com=with_com)
|
|
970
|
+
sw["checked_at"] = now_str
|
|
971
|
+
st["sw"] = sw
|
|
972
|
+
elif t == "cad":
|
|
973
|
+
cad = probe_cad(with_com=with_com)
|
|
974
|
+
cad["checked_at"] = now_str
|
|
975
|
+
st["cad"] = cad
|
|
976
|
+
elif t in _CAE_TARGETS:
|
|
977
|
+
# Abaqus / NX 共用通用 CAE 探测(都无 COM,走进程+安装探测)
|
|
978
|
+
_cae = probe_cae(t)
|
|
979
|
+
_cae["checked_at"] = now_str
|
|
980
|
+
st[t] = _cae
|
|
981
|
+
elif t.startswith("placeholder"):
|
|
982
|
+
# 占位项:如实记录"未接入",绝不伪装成已连接/已检测
|
|
983
|
+
st[t] = {
|
|
984
|
+
"running": None, "connected": False, "checked_at": now_str,
|
|
985
|
+
"placeholder": True,
|
|
986
|
+
"note": "该栏目为预留占位项,尚未接入探测逻辑",
|
|
987
|
+
}
|
|
988
|
+
else:
|
|
989
|
+
raise ValueError(
|
|
990
|
+
"target 必须是 'sw' / 'cad' / 'abaqus' / 'webots'"
|
|
991
|
+
" / 'placeholder*' 之一,收到: %r" % (target,))
|
|
992
|
+
# 路径一并刷新(若尚未解析过)
|
|
993
|
+
if not (st.get("paths") or {}).get("sldworks_exe"):
|
|
994
|
+
st["paths"] = discover_paths()
|
|
995
|
+
save(st)
|
|
996
|
+
return st
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
1000
|
+
# 判断(给流程用的代码级结论)
|
|
1001
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
1002
|
+
|
|
1003
|
+
# 全部合法的连接目标(新增目标只需在这里登记)
|
|
1004
|
+
# 【NX → Webots 改造】nx 已由 webots 取代;nx 作为【旧名别名】保留在
|
|
1005
|
+
# _TARGET_ALIASES 里,读到旧状态文件/旧调用时自动映射,不会报错。
|
|
1006
|
+
KNOWN_TARGETS = ("sw", "cad", "abaqus", "webots")
|
|
1007
|
+
# 旧名 → 新名(向后兼容;新增改名只需在这里登记一条)
|
|
1008
|
+
_TARGET_ALIASES = {
|
|
1009
|
+
"nx": "webots",
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _norm_target(target):
|
|
1014
|
+
"""归一化 target 名,非法时返回 None。
|
|
1015
|
+
|
|
1016
|
+
── 【连接区扩展修复】消除"静默当 cad"的隐患 ──────────────────────────
|
|
1017
|
+
原实现是 `"sw" if target == "sw" else "cad"` —— 任何非 sw 的输入
|
|
1018
|
+
(包括拼错、新增的 abaqus/nx)都会被【静默当成 cad】,
|
|
1019
|
+
于是查的是 CAD 的连接状态,却回报给调用方,属"安静地算错"。
|
|
1020
|
+
现在只认 KNOWN_TARGETS,未知一律返回 None,由调用方明确报错。
|
|
1021
|
+
"""
|
|
1022
|
+
t = str(target or "").strip().lower()
|
|
1023
|
+
# 【NX → Webots 改造】旧名先映射,再判合法(nx → webots)
|
|
1024
|
+
t = _TARGET_ALIASES.get(t, t)
|
|
1025
|
+
if t in KNOWN_TARGETS:
|
|
1026
|
+
return t
|
|
1027
|
+
if t.startswith("placeholder"):
|
|
1028
|
+
return t # 占位项允许通过(不参与连接判定)
|
|
1029
|
+
return None
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def is_connected(target, allow_stale=False):
|
|
1033
|
+
"""该目标是否【已连接】(机器可判定)。
|
|
1034
|
+
|
|
1035
|
+
allow_stale=False(默认)时,过期状态一律视为"未连接"——
|
|
1036
|
+
宁可多启动一次,也不要因陈旧状态跳过启动、最后在建模时莫名其妙失败。
|
|
1037
|
+
|
|
1038
|
+
⚠️ 未知 target 一律返回 False(而非"当成 cad"):宁可不判定,
|
|
1039
|
+
也不能把别的软件的状态当成它的。
|
|
1040
|
+
"""
|
|
1041
|
+
st = load()
|
|
1042
|
+
if is_stale(st) and not allow_stale:
|
|
1043
|
+
return False
|
|
1044
|
+
key = _norm_target(target)
|
|
1045
|
+
if not key:
|
|
1046
|
+
return False
|
|
1047
|
+
return bool((st.get(key) or {}).get("connected"))
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# ══ 【BUG-S01 修复】gate 的【实时进程探测】════════════════════════════════
|
|
1051
|
+
# 现场缺陷(2026-10-29 循迹小车任务 20261029_082936_4159A331,父代理实测):
|
|
1052
|
+
# SLDWORKS.exe 明明在运行(Get-Process 有 PID),但
|
|
1053
|
+
# python sw_bridge.py conn-gate --target sw
|
|
1054
|
+
# 返回 connected=false / skip_startup=false / stale=true,
|
|
1055
|
+
# state_age_sec=125645(connection_state.json 两天没刷新)。
|
|
1056
|
+
# 根因:gate() 完全信任 state 文件的时间戳,【从不看实时进程】——
|
|
1057
|
+
# 状态一旦过期就被判成"未连接",小屋于是重复走 SW 启动 7 步排查。
|
|
1058
|
+
# 修法(本段实现):
|
|
1059
|
+
# · 先做【实时探测】(只 tasklist / Get-Process,零 COM,绝不 Dispatch,
|
|
1060
|
+
# 所以"只想判断一下"绝不会把 SolidWorks 拉起来);
|
|
1061
|
+
# · 进程在 → connected=true / skip_startup=true / source="live_process";
|
|
1062
|
+
# · 进程不在 → 才回落到 state 文件判定(source="state_file");
|
|
1063
|
+
# · stale / state_age_sec 仍【如实】反映 state 文件新旧(不改 false 骗人)。
|
|
1064
|
+
# 探测手段本身失败(抛异常/命令不可用)→ 保守回退 state 文件,
|
|
1065
|
+
# 并在返回里带 live_probe_error,让调用方知道"这次没探到"而不是"没在跑"。
|
|
1066
|
+
|
|
1067
|
+
# 支持实时探测的目标(有稳定单进程名)。其余目标(abaqus/webots)仍以
|
|
1068
|
+
# state 文件为准 —— 它们没有"单进程名"这种可靠判据,贸然实时探测会误判。
|
|
1069
|
+
_LIVE_PROBE_TARGETS = ("sw", "cad")
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def _pids_of(procs):
|
|
1073
|
+
"""从进程记录里取 PID 列表(去 None / 容错转 int)。
|
|
1074
|
+
|
|
1075
|
+
同时容忍两种输入形态:{"pid": 1234} 字典记录,或直接就是 1234 的裸 PID
|
|
1076
|
+
(测试里常直接伪造 [1234],不该因为形态不同就判"探测失败")。
|
|
1077
|
+
"""
|
|
1078
|
+
out = []
|
|
1079
|
+
for p in (procs or []):
|
|
1080
|
+
try:
|
|
1081
|
+
if isinstance(p, dict):
|
|
1082
|
+
_pid = p.get("pid")
|
|
1083
|
+
elif isinstance(p, (int, str)):
|
|
1084
|
+
_pid = p
|
|
1085
|
+
else:
|
|
1086
|
+
_pid = getattr(p, "pid", None)
|
|
1087
|
+
if _pid is None:
|
|
1088
|
+
continue
|
|
1089
|
+
out.append(int(_pid))
|
|
1090
|
+
except Exception:
|
|
1091
|
+
continue
|
|
1092
|
+
return out
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _split_proc_result(res):
|
|
1096
|
+
"""把进程探测的返回值归一成 (procs, err)。
|
|
1097
|
+
|
|
1098
|
+
本模块 _sw_process_list() 返回 (procs, err);但调用方/测试也可能直接塞
|
|
1099
|
+
一个裸列表([{"pid": 1234}] 或 [1234])表示"进程列表"。两种都接受,
|
|
1100
|
+
只在拿到完全无法解释的东西时才报错 —— 免得"探测成功"被误判成失败。
|
|
1101
|
+
"""
|
|
1102
|
+
if isinstance(res, tuple) and len(res) == 2:
|
|
1103
|
+
return res[0], res[1]
|
|
1104
|
+
if res is None:
|
|
1105
|
+
return None, "进程探测返回 None"
|
|
1106
|
+
if isinstance(res, (list, tuple)):
|
|
1107
|
+
return res, None
|
|
1108
|
+
return None, "进程探测返回了无法解释的类型: %r" % (type(res).__name__,)
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _live_process_probe(target):
|
|
1112
|
+
"""【实时】枚举目标进程(零 COM / 零副作用)。
|
|
1113
|
+
|
|
1114
|
+
Returns:
|
|
1115
|
+
{"found": True/False/None, "pids": [int...], "method": str,
|
|
1116
|
+
"error": str|None}
|
|
1117
|
+
found=None + error 表示"探测手段本身失败"(≠ 进程不在),
|
|
1118
|
+
调用方必须保守回退到 state 文件,绝不据此判"未连接"。
|
|
1119
|
+
"""
|
|
1120
|
+
t = _norm_target(target)
|
|
1121
|
+
out = {"found": None, "pids": [], "method": None, "error": None}
|
|
1122
|
+
if t == "sw":
|
|
1123
|
+
out["method"] = "tasklist/Get-Process(conn_state._sw_process_list)"
|
|
1124
|
+
try:
|
|
1125
|
+
procs, err = _split_proc_result(_sw_process_list())
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
out["error"] = "实时进程探测异常: %r" % (e,)
|
|
1128
|
+
return out
|
|
1129
|
+
if err:
|
|
1130
|
+
# tasklist 与 PowerShell 回退都失败 → 无法判定,交给调用方回退
|
|
1131
|
+
out["error"] = str(err)
|
|
1132
|
+
return out
|
|
1133
|
+
out["pids"] = _pids_of(procs)
|
|
1134
|
+
out["found"] = bool(procs)
|
|
1135
|
+
return out
|
|
1136
|
+
if t == "cad":
|
|
1137
|
+
out["method"] = "tasklist(conn_state._acad_process_list)"
|
|
1138
|
+
try:
|
|
1139
|
+
procs, _cerr = _split_proc_result(_acad_process_list())
|
|
1140
|
+
# _acad_process_list() 自带异常吞掉,不会给 err;留字段以便将来扩展
|
|
1141
|
+
if _cerr:
|
|
1142
|
+
out["error"] = _cerr
|
|
1143
|
+
return out
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
out["error"] = "实时进程探测异常: %r" % (e,)
|
|
1146
|
+
return out
|
|
1147
|
+
out["pids"] = _pids_of(procs)
|
|
1148
|
+
out["found"] = bool(procs)
|
|
1149
|
+
return out
|
|
1150
|
+
# 其它目标不做实时探测(没有可靠单进程名判据)
|
|
1151
|
+
out["method"] = None
|
|
1152
|
+
return out
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def gate(target="sw"):
|
|
1156
|
+
"""【流程判断入口】返回可直接照着执行的分支结论。
|
|
1157
|
+
|
|
1158
|
+
这是用户要的"在流程中搞一个代码判断"的落点:
|
|
1159
|
+
· skip_startup=True → 连接区已连上,AI 直接进建模,跳过启动/排障
|
|
1160
|
+
· skip_startup=False → 按【原有流程】启动 SW(和 CAD),一步不动
|
|
1161
|
+
|
|
1162
|
+
返回 dict(全部字段都是给 AI/脚本直接读的):
|
|
1163
|
+
target, connected, skip_startup, next, paths, state_age_sec, stale, hint
|
|
1164
|
+
|
|
1165
|
+
── 【BUG-S01 修复】新增两个字段 ──────────────────────────────────────
|
|
1166
|
+
source : "live_process" | "state_file" —— 本次结论的来源。
|
|
1167
|
+
live_process 表示【进程确实在跑】(事实优先);
|
|
1168
|
+
state_file 表示实时没探到,按 state 文件口径判定。
|
|
1169
|
+
live_process : {found, pids, method, probed} —— 实时探测的证据。
|
|
1170
|
+
stale / state_age_sec 【不因实时探测而改值】:它们永远如实反映
|
|
1171
|
+
connection_state.json 的新旧(哪怕进程在跑、文件两天没更新)。
|
|
1172
|
+
探测手段本身失败时额外带 live_probe_error;state 与实时冲突时额外带
|
|
1173
|
+
live_conflict / live_conflict_note。
|
|
1174
|
+
"""
|
|
1175
|
+
st = load()
|
|
1176
|
+
t = _norm_target(target)
|
|
1177
|
+
if not t:
|
|
1178
|
+
# 未知 target:明确报错,绝不静默按 cad 处理
|
|
1179
|
+
return {
|
|
1180
|
+
"ok": False,
|
|
1181
|
+
"target": str(target),
|
|
1182
|
+
"error": "未知的连接目标 %r;合法值:%s"
|
|
1183
|
+
% (target, " / ".join(KNOWN_TARGETS)),
|
|
1184
|
+
"connected": False,
|
|
1185
|
+
"skip_startup": False,
|
|
1186
|
+
"next": "请用合法的 --target 重新调用(sw / cad / abaqus / nx)。",
|
|
1187
|
+
"hint": "连接区新增了 Abaqus / NX 目标;拼写错误会被显式拒绝,不再退化成 CAD。",
|
|
1188
|
+
}
|
|
1189
|
+
stale = is_stale(st)
|
|
1190
|
+
paths = st.get("paths") or {}
|
|
1191
|
+
entry = st.get(t) or {}
|
|
1192
|
+
|
|
1193
|
+
# ══ 【BUG-S01 修复】先【实时探测进程】,再看 state 文件 ═══════════════
|
|
1194
|
+
# 现场缺陷:SLDWORKS.exe 在跑,但 state 文件两天没刷新 → 旧实现只看
|
|
1195
|
+
# 时间戳,返回 connected=false / skip_startup=false(小屋白走启动排障)。
|
|
1196
|
+
# 新判据(优先级从高到低):
|
|
1197
|
+
# ① 实时进程在 → connected=true + source="live_process"
|
|
1198
|
+
# ② 实时探测说"不在" → 才回落到 state 文件(source="state_file",
|
|
1199
|
+
# stale / state_age_sec 仍如实反映 state 文件新旧,不改 false 骗人)
|
|
1200
|
+
# ③ 实时探测【本身失败】(found=None + error)→ 保守回退 state 文件,
|
|
1201
|
+
# 返回体带 live_probe_error("没探到"≠"没在跑",不能据此判未连接)
|
|
1202
|
+
_live = _live_process_probe(t)
|
|
1203
|
+
_live_err = _live.get("error")
|
|
1204
|
+
_state_connected = is_connected(t)
|
|
1205
|
+
_live_usable = (t in _LIVE_PROBE_TARGETS) and (_live.get("found") is not None)
|
|
1206
|
+
# 【真实冲突】state 文件说"已连接",实时探测却说"进程不在" ——
|
|
1207
|
+
# 按需求口径落到 state 判定,但必须显式标出来,不许悄悄当成没事。
|
|
1208
|
+
_live_conflict = bool(_live_usable and (_live.get("found") is False)
|
|
1209
|
+
and _state_connected)
|
|
1210
|
+
if _live_usable and _live.get("found"):
|
|
1211
|
+
# ① 实时进程在 → 事实优先,直接判已连接(这就是 S01 的修复点)
|
|
1212
|
+
connected = True
|
|
1213
|
+
source = "live_process"
|
|
1214
|
+
else:
|
|
1215
|
+
# ② 进程不在 / 探测失败 → 回落到原来的 state 文件判定
|
|
1216
|
+
# (stale 时的"过期即未连接"保护仍在 is_connected 里)
|
|
1217
|
+
connected = _state_connected
|
|
1218
|
+
source = "state_file"
|
|
1219
|
+
skip = bool(connected)
|
|
1220
|
+
|
|
1221
|
+
# 各目标的"下一步"文案(新增目标在此登记,避免落到默认分支说错话)
|
|
1222
|
+
_NEXT_OK = {
|
|
1223
|
+
"sw": "直接开始建模(跳过启动与排障)。",
|
|
1224
|
+
"cad": "直接使用现有 AutoCAD(跳过启动与排障)。",
|
|
1225
|
+
"abaqus": ("检测到 Abaqus 可用 —— 可直接用 `abaqus python <脚本>` "
|
|
1226
|
+
"或 `abaqus cae noGUI=<脚本>` 提交分析。"),
|
|
1227
|
+
"webots": ("检测到 Webots 可用 —— 可直接用 `webots --mode=fast "
|
|
1228
|
+
"<世界.wbt>` 启动仿真,外部控制走 webots-controller。"),
|
|
1229
|
+
"nx": ("检测到 NX 可用 —— 可直接用 `run_journal <脚本.py>`"
|
|
1230
|
+
"(NX Open 批处理)提交作业。"),
|
|
1231
|
+
}
|
|
1232
|
+
_NEXT_NO = {
|
|
1233
|
+
"sw": ("按原有流程启动 SolidWorks(含《常见SW启动失败问题》7 步排查),"
|
|
1234
|
+
"启动后再建模。"),
|
|
1235
|
+
"cad": "按原有流程启动 AutoCAD。",
|
|
1236
|
+
"abaqus": ("未检测到 Abaqus —— 请在【设置 → 连接区】点『Abaqus连接』"
|
|
1237
|
+
"探测安装路径,或先安装/启动 Abaqus。"),
|
|
1238
|
+
"webots": ("未检测到 Webots —— 请在【设置 → 连接区】点『Webots连接』"
|
|
1239
|
+
"探测安装路径,或先安装/启动 Webots。"),
|
|
1240
|
+
"nx": ("未检测到 NX —— 请在【设置 → 连接区】点『NX连接』"
|
|
1241
|
+
"探测安装路径,或先安装/启动 NX。"),
|
|
1242
|
+
}
|
|
1243
|
+
if connected:
|
|
1244
|
+
nxt = _NEXT_OK.get(t, "目标已连接,可直接使用。")
|
|
1245
|
+
skip = True
|
|
1246
|
+
else:
|
|
1247
|
+
nxt = _NEXT_NO.get(t, "目标未连接,请先启动或安装。")
|
|
1248
|
+
skip = False
|
|
1249
|
+
|
|
1250
|
+
_age = age_sec(st)
|
|
1251
|
+
hint = None
|
|
1252
|
+
if connected and source == "live_process" and stale:
|
|
1253
|
+
# 进程在、但 state 文件旧 —— 这正是 S01 的现场:如实说明"以实时为准"
|
|
1254
|
+
hint = ("已按【实时进程】判定为已连接(PID: %s);连接状态文件本身已过期"
|
|
1255
|
+
"(%.0f 秒前),stale 字段仍如实为 true —— 如需刷新元数据可运行 "
|
|
1256
|
+
"python sw_bridge.py conn-probe --target %s。"
|
|
1257
|
+
% (", ".join(str(x) for x in (_live.get("pids") or [])) or "?",
|
|
1258
|
+
_age or 0, t))
|
|
1259
|
+
elif not connected and stale and (entry.get("checked_at")):
|
|
1260
|
+
hint = ("连接状态已过期(%.0f 秒前),判定为未连接 —— "
|
|
1261
|
+
"可先运行 conn-probe 重新探测。" % (_age or 0))
|
|
1262
|
+
elif not connected and not entry.get("checked_at"):
|
|
1263
|
+
hint = ("从未探测过连接状态。请先在【设置 → 连接区】点『连接』,"
|
|
1264
|
+
"或运行 python sw_bridge.py conn-probe --target %s。" % t)
|
|
1265
|
+
if not connected and _live_err:
|
|
1266
|
+
hint = ((hint + " ") if hint else "") + (
|
|
1267
|
+
"另:实时进程探测本身失败(%s),本次结论来自 state 文件,"
|
|
1268
|
+
"不能据此断定进程没在运行。" % _live_err)
|
|
1269
|
+
if connected and source == "state_file":
|
|
1270
|
+
# state 文件说连接、但没能实时确认 —— 提醒调用方"这是记忆,不是事实"
|
|
1271
|
+
_n = "连接结论来自 state 文件(%s),本次未取得实时进程证据" % (
|
|
1272
|
+
entry.get("checked_at") or "无时间戳")
|
|
1273
|
+
_n += "(实时探测失败: %s)。" % _live_err if _live_err else (
|
|
1274
|
+
"(该目标不支持实时进程探测)。")
|
|
1275
|
+
hint = ((hint + " ") if hint else "") + _n
|
|
1276
|
+
|
|
1277
|
+
out = {
|
|
1278
|
+
"ok": True,
|
|
1279
|
+
"target": t,
|
|
1280
|
+
"connected": connected,
|
|
1281
|
+
"skip_startup": skip,
|
|
1282
|
+
"next": nxt,
|
|
1283
|
+
# ── 【BUG-S01】stale / state_age_sec 仍【如实】反映 state 文件新旧 ──
|
|
1284
|
+
"stale": stale,
|
|
1285
|
+
"state_age_sec": _age,
|
|
1286
|
+
"checked_at": entry.get("checked_at"),
|
|
1287
|
+
"paths": paths,
|
|
1288
|
+
"hint": hint,
|
|
1289
|
+
# ── 新增:本次结论的【来源】与实时进程证据(AI 可据此判断可信度)──
|
|
1290
|
+
"source": source,
|
|
1291
|
+
"live_process": {
|
|
1292
|
+
"found": _live.get("found"),
|
|
1293
|
+
"pids": _live.get("pids") or [],
|
|
1294
|
+
"method": _live.get("method"),
|
|
1295
|
+
"probed": bool(_live_usable),
|
|
1296
|
+
},
|
|
1297
|
+
}
|
|
1298
|
+
if _live_err:
|
|
1299
|
+
out["live_probe_error"] = _live_err
|
|
1300
|
+
if _live_conflict:
|
|
1301
|
+
out["live_conflict"] = True
|
|
1302
|
+
out["live_conflict_note"] = (
|
|
1303
|
+
"state 文件记录为已连接,但实时探测未发现进程(PID 历史: %s)——"
|
|
1304
|
+
"可能是进程已被关掉、或探测手段受限。若确认 SW 在跑,"
|
|
1305
|
+
"请重试 python sw_bridge.py sw-proc 并运行 conn-probe --target %s。"
|
|
1306
|
+
% (", ".join(str(x) for x in (_live.get("pids") or [])) or "无", t))
|
|
1307
|
+
# 把"该用哪个 exe"直接给出来 —— 免得 AI 再去全盘找(诉求2)
|
|
1308
|
+
_EXE_KEY = {"sw": "sldworks_exe", "cad": "acad_exe",
|
|
1309
|
+
"abaqus": "abaqus_exe", "nx": "nx_exe",
|
|
1310
|
+
"webots": "webots_exe"}
|
|
1311
|
+
out["exe"] = paths.get(_EXE_KEY.get(t, "")) or entry.get("exe")
|
|
1312
|
+
if not out["exe"]:
|
|
1313
|
+
out["exe_hint"] = ("尚未解析到安装路径,可运行 "
|
|
1314
|
+
"python sw_bridge.py conn-probe --resolve-paths")
|
|
1315
|
+
return out
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def summary():
|
|
1319
|
+
"""人读友好的连接区状态摘要(供 UI/CLI 展示)。"""
|
|
1320
|
+
st = load()
|
|
1321
|
+
a = age_sec(st)
|
|
1322
|
+
return {
|
|
1323
|
+
"ok": True,
|
|
1324
|
+
"state_file": state_path(),
|
|
1325
|
+
"updated_at": st.get("updated_at_str"),
|
|
1326
|
+
"age_sec": a,
|
|
1327
|
+
"stale": is_stale(st),
|
|
1328
|
+
"sw": st.get("sw"),
|
|
1329
|
+
"cad": st.get("cad"),
|
|
1330
|
+
# 【连接区扩展】Abaqus / Webots 也要透给 UI(前端按 state[target] 读)
|
|
1331
|
+
"abaqus": st.get("abaqus"),
|
|
1332
|
+
"webots": st.get("webots") or st.get("nx"),
|
|
1333
|
+
"paths": st.get("paths"),
|
|
1334
|
+
"launch": st.get("launch"),
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
1339
|
+
# 自动启动(「启动」按钮)
|
|
1340
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
1341
|
+
# 用户诉求原文:
|
|
1342
|
+
# "要不给俩个都搞一个按键,自动启动吧,不然一直点连接启动不了的,
|
|
1343
|
+
# 就是启动成不需要 SW 自己跳出来的那种,然后要是报错的话,就贴出错误日志,
|
|
1344
|
+
# 然后加上一个按键就是,按了就可以将错误日志直接发给 DSH 内部模型的,
|
|
1345
|
+
# 然后 DSH 就可以根据错误日志去找到底是啥情况。"
|
|
1346
|
+
#
|
|
1347
|
+
# 设计要点:
|
|
1348
|
+
# ① 「连接」= 只探测(安全,默认);「启动」= 显式启动进程(用户主动点击)。
|
|
1349
|
+
# 两者分开,避免"只想看看"却把几十秒的 CAD/SW 拉起来。
|
|
1350
|
+
# ② 「不需要 SW 自己跳出来」= 启动后调用 _show_main_window(),
|
|
1351
|
+
# 它会 SW_RESTORE + 最大化 主窗口,并【隐藏欢迎页】。
|
|
1352
|
+
# ③ 全流程留日志:每一步(找路径 / 拉起进程 / 等窗口 / 连 COM)都记进
|
|
1353
|
+
# state["launch"][target],包含 stdout/stderr/returncode/耗时。
|
|
1354
|
+
# 失败时 UI 能直接贴出日志,并能一键发给 DSH 模型诊断。
|
|
1355
|
+
|
|
1356
|
+
def _log_launch(target, ok, steps, error=None):
|
|
1357
|
+
"""把一次启动尝试的日志写入状态文件(覆盖上一次)。"""
|
|
1358
|
+
st = load()
|
|
1359
|
+
st.setdefault("launch", {})
|
|
1360
|
+
st["launch"][target] = {
|
|
1361
|
+
"ok": bool(ok),
|
|
1362
|
+
"at": time.time(),
|
|
1363
|
+
"at_str": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
1364
|
+
"error": error,
|
|
1365
|
+
# steps 是"步骤名 → {ok, detail, ...}"的有序列表,便于 UI 逐条展示
|
|
1366
|
+
"steps": steps,
|
|
1367
|
+
}
|
|
1368
|
+
save(st)
|
|
1369
|
+
return st["launch"][target]
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def _step_result(name, ok, **kw):
|
|
1373
|
+
"""构造一条启动步骤记录。"""
|
|
1374
|
+
d = {"step": name, "ok": bool(ok)}
|
|
1375
|
+
d.update(kw)
|
|
1376
|
+
return d
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _drive_hint():
|
|
1380
|
+
"""返回"本次扫描了哪些盘符"的可读串(供错误信息展示)。"""
|
|
1381
|
+
try:
|
|
1382
|
+
import swapi
|
|
1383
|
+
_d = swapi._drive_roots()
|
|
1384
|
+
return ", ".join(x.rstrip("\\") for x in _d) if _d else "C:"
|
|
1385
|
+
except Exception:
|
|
1386
|
+
return "C:"
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def launch(target, exe=None, wait_window=120, connect=True):
|
|
1390
|
+
"""启动 SW / CAD 并(可选)连接,全程记录日志。
|
|
1391
|
+
|
|
1392
|
+
Args:
|
|
1393
|
+
target: 'sw' | 'cad'
|
|
1394
|
+
exe: 可执行文件路径(None 则自动解析)
|
|
1395
|
+
wait_window: 等待主窗口/进程就绪的最长秒数
|
|
1396
|
+
connect: 启动后是否再走一次探测+连接
|
|
1397
|
+
|
|
1398
|
+
Returns: {"ok":..., "target":..., "steps":[...], "error":..., "state":...}
|
|
1399
|
+
"""
|
|
1400
|
+
import subprocess
|
|
1401
|
+
# ── 【NX → Webots 改造】不再把「非 sw」一律当成 cad ────────────────
|
|
1402
|
+
# 原实现 `t = "sw" if ...=="sw" else "cad"` 会让 webots/abaqus 的启动
|
|
1403
|
+
# 去拉起 AutoCAD —— 安静地做错事。现在按 _norm_target 归一,
|
|
1404
|
+
# 不认识的直接如实报错。
|
|
1405
|
+
t = _norm_target(target)
|
|
1406
|
+
if not t:
|
|
1407
|
+
return {"ok": False, "target": str(target), "steps": [],
|
|
1408
|
+
"error": ("target 必须是 'sw' / 'cad' / 'abaqus' / "
|
|
1409
|
+
"'webots' 之一,收到: %r" % (target,))}
|
|
1410
|
+
steps = []
|
|
1411
|
+
|
|
1412
|
+
# ── 步骤 1:解析可执行文件路径(全盘符,任意盘都能找到)──────────────
|
|
1413
|
+
if not exe:
|
|
1414
|
+
_EXE_OF = {"sw": "sldworks_exe", "cad": "acad_exe",
|
|
1415
|
+
"abaqus": "abaqus_exe", "webots": "webots_exe"}
|
|
1416
|
+
try:
|
|
1417
|
+
paths = discover_paths()
|
|
1418
|
+
exe = (paths or {}).get(_EXE_OF.get(t, ""))
|
|
1419
|
+
# 【连接区扩展】CAE 型目标(Abaqus / Webots)若 paths 里还没有,
|
|
1420
|
+
# 现场再找一次(discover_paths 的缓存可能早于本目标被登记)。
|
|
1421
|
+
if not exe and t in _CAE_TARGETS:
|
|
1422
|
+
exe = _find_cae_exe(_CAE_TARGETS[t])
|
|
1423
|
+
except Exception as e:
|
|
1424
|
+
exe = None
|
|
1425
|
+
steps.append(_step_result("resolve_path", False, error=repr(e)))
|
|
1426
|
+
else:
|
|
1427
|
+
steps.append(_step_result(
|
|
1428
|
+
"resolve_path", bool(exe),
|
|
1429
|
+
exe=exe,
|
|
1430
|
+
detail=("已定位可执行文件" if exe else
|
|
1431
|
+
"未找到安装路径(已扫描全部盘符)")))
|
|
1432
|
+
else:
|
|
1433
|
+
steps.append(_step_result("resolve_path", os.path.exists(exe), exe=exe))
|
|
1434
|
+
|
|
1435
|
+
if not exe or not os.path.exists(exe):
|
|
1436
|
+
_EXE_NAME = {"sw": "SLDWORKS.exe", "cad": "acad.exe",
|
|
1437
|
+
"abaqus": "abaqus.bat / ABQcaeK.exe",
|
|
1438
|
+
"webots": "webots.exe"}
|
|
1439
|
+
_ENV_NAME = {"sw": "DSH_SW_EXE", "cad": "DSH_ACAD_EXE",
|
|
1440
|
+
"abaqus": "DSH_ABAQUS_EXE", "webots": "DSH_WEBOTS_EXE"}
|
|
1441
|
+
_exe_name = _EXE_NAME.get(t, t)
|
|
1442
|
+
_env_name = _ENV_NAME.get(t, "DSH_" + t.upper() + "_EXE")
|
|
1443
|
+
_hint_reg = ("· 若软件确实已安装,多为此类情况:注册表里没有完整安装记录,"
|
|
1444
|
+
"或装在【非常规目录】(例如 Z:\\7-Zip\\AutoCAD 2020)。"
|
|
1445
|
+
if t == "cad" else
|
|
1446
|
+
"· 若软件确实已安装,多为注册表缺少安装记录,"
|
|
1447
|
+
"或装在非常规目录。")
|
|
1448
|
+
err = ("未找到 %s。\n"
|
|
1449
|
+
"已尝试:环境变量 %s → 注册表递归搜索 → 运行中进程反推 → "
|
|
1450
|
+
"全部盘符(%s)常见目录扫描。\n"
|
|
1451
|
+
"可能原因:① 软件未安装;② 安装在非常规目录;"
|
|
1452
|
+
"③ 安装目录无读取权限;④ 注册表无安装记录。\n"
|
|
1453
|
+
"%s\n"
|
|
1454
|
+
"【最可靠的办法】显式指定路径(支持指向 exe 本身或所在目录):\n"
|
|
1455
|
+
" PowerShell: $env:%s = 'D:\\你的安装目录\\acad.exe'\n"
|
|
1456
|
+
" 或在系统环境变量里新建 %s。"
|
|
1457
|
+
% (_exe_name, _env_name, _drive_hint(), _hint_reg, _env_name,
|
|
1458
|
+
_env_name))
|
|
1459
|
+
steps.append(_step_result("resolve_path", False, error=err))
|
|
1460
|
+
_log_launch(t, False, steps, error=err)
|
|
1461
|
+
return {"ok": False, "target": t, "steps": steps, "error": err}
|
|
1462
|
+
|
|
1463
|
+
# ── 步骤 2:是否已在运行(已在运行就不重复启动)─────────────────────
|
|
1464
|
+
already = False
|
|
1465
|
+
try:
|
|
1466
|
+
if t == "sw":
|
|
1467
|
+
already = bool(_sw_process_list()[0])
|
|
1468
|
+
else:
|
|
1469
|
+
already = bool(_acad_process_list())
|
|
1470
|
+
except Exception:
|
|
1471
|
+
already = False
|
|
1472
|
+
steps.append(_step_result("check_running", True, already_running=already,
|
|
1473
|
+
detail=("已在运行,跳过启动" if already
|
|
1474
|
+
else "未运行,需要启动")))
|
|
1475
|
+
|
|
1476
|
+
proc_info = None
|
|
1477
|
+
if not already:
|
|
1478
|
+
# ── 步骤 3:拉起进程(分离启动,不阻塞本进程)──────────────────
|
|
1479
|
+
try:
|
|
1480
|
+
# DETACHED_PROCESS + CREATE_NEW_PROCESS_GROUP:
|
|
1481
|
+
# 让 SW/CAD 独立于本 Python 进程存活,Python 退出不带走它。
|
|
1482
|
+
_flags = 0
|
|
1483
|
+
if sys.platform == "win32":
|
|
1484
|
+
_flags = (0x00000008 | 0x00000200) # DETACHED | NEW_GROUP
|
|
1485
|
+
p = subprocess.Popen(
|
|
1486
|
+
[exe], cwd=os.path.dirname(exe),
|
|
1487
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
1488
|
+
stdin=subprocess.DEVNULL,
|
|
1489
|
+
creationflags=_flags, close_fds=True)
|
|
1490
|
+
proc_info = {"pid": p.pid}
|
|
1491
|
+
steps.append(_step_result("spawn", True, pid=p.pid,
|
|
1492
|
+
detail="已拉起进程 PID %s" % p.pid))
|
|
1493
|
+
except Exception as e:
|
|
1494
|
+
err = "拉起进程失败: %r" % (e,)
|
|
1495
|
+
steps.append(_step_result("spawn", False, error=err))
|
|
1496
|
+
_log_launch(t, False, steps, error=err)
|
|
1497
|
+
return {"ok": False, "target": t, "steps": steps, "error": err}
|
|
1498
|
+
|
|
1499
|
+
# ── 步骤 4:等待就绪 ─────────────────────────────────────────
|
|
1500
|
+
# SW:等主窗口出现(可能几十秒,首次启动更久)
|
|
1501
|
+
# CAD:等进程出现即可(CAD 的 COM 激活本身会等)
|
|
1502
|
+
deadline = time.time() + max(10, int(wait_window))
|
|
1503
|
+
appeared = False
|
|
1504
|
+
_last = None
|
|
1505
|
+
while time.time() < deadline:
|
|
1506
|
+
try:
|
|
1507
|
+
if t == "sw":
|
|
1508
|
+
lst, _e = _sw_process_list()
|
|
1509
|
+
appeared = bool(lst)
|
|
1510
|
+
_last = lst
|
|
1511
|
+
elif t == "cad":
|
|
1512
|
+
_last = _acad_process_list()
|
|
1513
|
+
appeared = bool(_last)
|
|
1514
|
+
else:
|
|
1515
|
+
# 【连接区扩展】Abaqus / Webots:用各自的关键词表等进程出现
|
|
1516
|
+
_last = (_CAE_TARGETS.get(t, {}).get("process_fn")
|
|
1517
|
+
or (lambda: []))()
|
|
1518
|
+
appeared = bool(_last)
|
|
1519
|
+
except Exception:
|
|
1520
|
+
appeared = False
|
|
1521
|
+
if appeared:
|
|
1522
|
+
break
|
|
1523
|
+
time.sleep(2.0)
|
|
1524
|
+
steps.append(_step_result(
|
|
1525
|
+
"wait_ready", appeared,
|
|
1526
|
+
waited_sec=round(max(0.0, wait_window - (deadline - time.time())), 1),
|
|
1527
|
+
processes=_last or [],
|
|
1528
|
+
detail=("进程已就绪" if appeared
|
|
1529
|
+
else "等待超时:进程未出现")))
|
|
1530
|
+
if not appeared:
|
|
1531
|
+
err = ("进程已拉起但 %d 秒内未出现在进程列表中。\n"
|
|
1532
|
+
"常见原因:① 许可证缺失/失效(sw_d.lic);"
|
|
1533
|
+
"② 缺少 netapi32.dll;③ 被杀软/沙箱拦截;"
|
|
1534
|
+
"④ 首次启动极慢(可再等一会儿后点『连接』重试)。"
|
|
1535
|
+
% wait_window)
|
|
1536
|
+
_log_launch(t, False, steps, error=err)
|
|
1537
|
+
return {"ok": False, "target": t, "steps": steps, "error": err}
|
|
1538
|
+
|
|
1539
|
+
# 给 GUI 一点初始化时间,避免立刻 COM 连接被拒
|
|
1540
|
+
time.sleep(3.0)
|
|
1541
|
+
else:
|
|
1542
|
+
proc_info = {"already_running": True}
|
|
1543
|
+
|
|
1544
|
+
# ── 步骤 5:让它"体面地出现"(隐藏欢迎页 / 最大化主窗口)─────────────
|
|
1545
|
+
# SW 的欢迎页是独立小窗,会挡住视线;用户明确要"不需要 SW 自己跳出来"。
|
|
1546
|
+
if t == "sw":
|
|
1547
|
+
try:
|
|
1548
|
+
import swapi
|
|
1549
|
+
swapi._wait_sw_window(timeout=60)
|
|
1550
|
+
swapi._show_main_window(maximize=True)
|
|
1551
|
+
steps.append(_step_result("show_window", True,
|
|
1552
|
+
detail="主窗口已置前并最大化,欢迎页已隐藏"))
|
|
1553
|
+
except Exception as e:
|
|
1554
|
+
# 窗口处理失败不算致命:SW 可能仍在初始化
|
|
1555
|
+
steps.append(_step_result("show_window", False, error=repr(e),
|
|
1556
|
+
detail="窗口处理失败(不影响连接尝试)"))
|
|
1557
|
+
|
|
1558
|
+
# ── 步骤 6:连接确认(读属性以验证 COM 真的可用)────────────────────
|
|
1559
|
+
connected = False
|
|
1560
|
+
if connect:
|
|
1561
|
+
try:
|
|
1562
|
+
if t == "sw":
|
|
1563
|
+
res = probe_sw(with_com=True)
|
|
1564
|
+
elif t == "cad":
|
|
1565
|
+
res = probe_cad(with_com=True)
|
|
1566
|
+
else:
|
|
1567
|
+
# 【连接区扩展】Abaqus / Webots 无 COM:connected 语义是
|
|
1568
|
+
# "安装可用或进程在跑",不是"COM 会话已建立"。
|
|
1569
|
+
res = probe_cae(t)
|
|
1570
|
+
connected = bool(res.get("connected"))
|
|
1571
|
+
steps.append(_step_result(
|
|
1572
|
+
"connect", connected,
|
|
1573
|
+
detail=(("COM 连接成功" if connected else "COM 连接失败")
|
|
1574
|
+
if t in ("sw", "cad") else
|
|
1575
|
+
("已确认可用(无 COM 接口,走命令行/控制器)"
|
|
1576
|
+
if connected else "未确认可用(未找到安装且进程未运行)")),
|
|
1577
|
+
**{k: v for k, v in res.items()
|
|
1578
|
+
if k in ("error", "revision", "binding", "pid", "drawing",
|
|
1579
|
+
"exe", "note", "interface")}))
|
|
1580
|
+
if not connected and res.get("error"):
|
|
1581
|
+
_log_launch(t, False, steps, error=str(res.get("error")))
|
|
1582
|
+
return {"ok": False, "target": t, "steps": steps,
|
|
1583
|
+
"error": str(res.get("error")), "state": summary()}
|
|
1584
|
+
except Exception as e:
|
|
1585
|
+
steps.append(_step_result("connect", False, error=repr(e)))
|
|
1586
|
+
_log_launch(t, False, steps, error=repr(e))
|
|
1587
|
+
return {"ok": False, "target": t, "steps": steps,
|
|
1588
|
+
"error": repr(e), "state": summary()}
|
|
1589
|
+
|
|
1590
|
+
# 启动成功后再探一次,刷新状态文件(进程层信息也一并更新)
|
|
1591
|
+
try:
|
|
1592
|
+
probe_target(t, with_com=connect)
|
|
1593
|
+
except Exception:
|
|
1594
|
+
pass
|
|
1595
|
+
|
|
1596
|
+
_log_launch(t, True, steps, error=None)
|
|
1597
|
+
return {"ok": True, "target": t, "steps": steps, "connected": connected,
|
|
1598
|
+
"error": None, "state": summary()}
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def launch_log(target):
|
|
1602
|
+
"""读取某目标最近一次启动日志(供 UI 展示 / 发给模型诊断)。"""
|
|
1603
|
+
st = load()
|
|
1604
|
+
t = _norm_target(target) or str(target or "").strip().lower() or "sw"
|
|
1605
|
+
lg = (st.get("launch") or {}).get(t)
|
|
1606
|
+
if not lg:
|
|
1607
|
+
return {"ok": False, "target": t, "error": "尚无启动日志"}
|
|
1608
|
+
return {"ok": True, "target": t, "log": lg}
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def format_launch_log(target, for_model=True):
|
|
1612
|
+
"""把启动日志渲染成【可读文本】,供 UI 展示或直接发给 DSH 模型诊断。
|
|
1613
|
+
|
|
1614
|
+
for_model=True 时输出面向模型的诊断材料:包含步骤、错误、环境事实,
|
|
1615
|
+
让模型能据此判断"到底是许可证问题还是路径问题还是沙箱问题"。
|
|
1616
|
+
"""
|
|
1617
|
+
st = load()
|
|
1618
|
+
t = _norm_target(target) or str(target or "").strip().lower() or "sw"
|
|
1619
|
+
lg = (st.get("launch") or {}).get(t)
|
|
1620
|
+
_NAMES = {"sw": "SolidWorks", "cad": "AutoCAD",
|
|
1621
|
+
"abaqus": "Abaqus", "webots": "Webots"}
|
|
1622
|
+
name = _NAMES.get(t, t)
|
|
1623
|
+
lines = []
|
|
1624
|
+
if not lg:
|
|
1625
|
+
return "[%s] 尚无启动日志 —— 请先在设置页点『启动』。" % name
|
|
1626
|
+
lines.append("=== %s 启动日志 ===" % name)
|
|
1627
|
+
lines.append("时间 : %s" % lg.get("at_str"))
|
|
1628
|
+
lines.append("结果 : %s" % ("成功" if lg.get("ok") else "失败"))
|
|
1629
|
+
if lg.get("error"):
|
|
1630
|
+
lines.append("错误 : %s" % lg.get("error"))
|
|
1631
|
+
lines.append("")
|
|
1632
|
+
lines.append("--- 步骤明细 ---")
|
|
1633
|
+
for s in (lg.get("steps") or []):
|
|
1634
|
+
mark = "OK " if s.get("ok") else "FAIL"
|
|
1635
|
+
line = "[%s] %s" % (mark, s.get("step"))
|
|
1636
|
+
for k in ("detail", "error", "exe", "pid", "waited_sec",
|
|
1637
|
+
"already_running", "revision", "binding"):
|
|
1638
|
+
if s.get(k) not in (None, "", []):
|
|
1639
|
+
line += "\n %s: %s" % (k, s.get(k))
|
|
1640
|
+
lines.append(line)
|
|
1641
|
+
if for_model:
|
|
1642
|
+
lines.append("")
|
|
1643
|
+
lines.append("--- 环境事实(供诊断)---")
|
|
1644
|
+
try:
|
|
1645
|
+
import swapi
|
|
1646
|
+
lines.append("盘符扫描 : %s" % (swapi._drive_roots(),))
|
|
1647
|
+
lines.append("SW 可执行 : %s" % swapi.find_sldworks_exe())
|
|
1648
|
+
except Exception as e:
|
|
1649
|
+
lines.append("SW 探测异常 : %r" % (e,))
|
|
1650
|
+
_p = st.get("paths") or {}
|
|
1651
|
+
lines.append("CAD 可执行 : %s" % _p.get("acad_exe"))
|
|
1652
|
+
try:
|
|
1653
|
+
import platform
|
|
1654
|
+
lines.append("Python : %s (%s)" % (
|
|
1655
|
+
platform.python_version(), platform.platform()))
|
|
1656
|
+
except Exception:
|
|
1657
|
+
pass
|
|
1658
|
+
try:
|
|
1659
|
+
import win32com
|
|
1660
|
+
lines.append("pywin32 : %s" % win32com.__file__)
|
|
1661
|
+
except Exception as e:
|
|
1662
|
+
lines.append("pywin32 : 缺失 %r" % (e,))
|
|
1663
|
+
_swm = st.get("sw") or {}
|
|
1664
|
+
_cadm = st.get("cad") or {}
|
|
1665
|
+
lines.append("SW 进程/连接 : running=%s connected=%s pid=%s" % (
|
|
1666
|
+
_swm.get("running"), _swm.get("connected"), _swm.get("pid")))
|
|
1667
|
+
lines.append("CAD 进程/连接 : running=%s connected=%s pid=%s" % (
|
|
1668
|
+
_cadm.get("running"), _cadm.get("connected"), _cadm.get("pid")))
|
|
1669
|
+
lines.append("")
|
|
1670
|
+
lines.append("请据此判断失败根因,并给出下一步修复动作。")
|
|
1671
|
+
return "\n".join(lines)
|