@aryacode/dsh-engineering 0.0.0-stage → 0.4.0
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/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/cordis.patch.yml +123 -0
- package/lib/client.js +3448 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Native variable records adapted from the supplied MotionWorksTools workflow.
|
|
2
|
+
|
|
3
|
+
Four UTF-16 cells plus a 16-byte record tail; the group trailer is NOT a record.
|
|
4
|
+
Unknown bytes between native records are retained. Donors must match type and usage.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import re
|
|
8
|
+
import struct
|
|
9
|
+
from .errors import UnsupportedFormat, NotFound
|
|
10
|
+
|
|
11
|
+
USAGES = (1, 5, 0x40001, 6, 22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_record(raw: bytes, offset: int) -> dict:
|
|
15
|
+
if offset < 12 or offset + 24 > len(raw):
|
|
16
|
+
raise UnsupportedFormat('Truncated variable record header')
|
|
17
|
+
fields = struct.unpack_from('<6I', raw, offset)
|
|
18
|
+
pos, cells, spans = offset + 24, [], []
|
|
19
|
+
for _ in range(4):
|
|
20
|
+
if pos + 4 > len(raw):
|
|
21
|
+
raise UnsupportedFormat('Truncated variable cell length')
|
|
22
|
+
size = struct.unpack_from('<I', raw, pos)[0]
|
|
23
|
+
if size < 2 or size % 2 or size > 65536 or pos + 4 + size > len(raw):
|
|
24
|
+
raise UnsupportedFormat('Invalid UTF-16 variable cell length')
|
|
25
|
+
data = raw[pos + 4:pos + 4 + size]
|
|
26
|
+
if not data.endswith(b'\0\0'):
|
|
27
|
+
raise UnsupportedFormat('Variable cell lacks a terminator')
|
|
28
|
+
try:
|
|
29
|
+
value = data[:-2].decode('utf-16le')
|
|
30
|
+
except UnicodeDecodeError as exc:
|
|
31
|
+
raise UnsupportedFormat('Invalid UTF-16 variable cell') from exc
|
|
32
|
+
if '\0' in value:
|
|
33
|
+
raise UnsupportedFormat('Embedded NUL in variable cell')
|
|
34
|
+
spans.append((pos, pos + 4, pos + 4 + size))
|
|
35
|
+
cells.append(value)
|
|
36
|
+
pos += 4 + size
|
|
37
|
+
if pos + 16 > len(raw):
|
|
38
|
+
raise UnsupportedFormat('Truncated variable record tail')
|
|
39
|
+
return dict(offset=offset, end=pos + 16, fields=fields, handle=fields[0], usage=fields[1],
|
|
40
|
+
group=fields[2], row=fields[4], cells=cells, spans=spans,
|
|
41
|
+
type=cells[0], address=cells[1], initial_value=cells[2], name=cells[3],
|
|
42
|
+
tail=raw[pos:pos + 16], type_at=spans[0][0], between_at=spans[0][2],
|
|
43
|
+
initial_at=spans[2][0], name_at=spans[3][0], after_name=pos, strings_end=pos)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse(raw: bytes) -> list[dict]:
|
|
47
|
+
if len(raw) < 12 or struct.unpack_from('<I', raw)[0] != 524289:
|
|
48
|
+
raise UnsupportedFormat('Unrecognized native variable worksheet header')
|
|
49
|
+
high, count = struct.unpack_from('<2I', raw, 4)
|
|
50
|
+
records = []
|
|
51
|
+
for offset in range(12, max(12, len(raw) - 23)):
|
|
52
|
+
h, u, g, f, row, z = struct.unpack_from('<6I', raw, offset)
|
|
53
|
+
if not (1000 <= h <= high and u in USAGES and 1 <= g <= 32 and f == z == 0 and 0 < row < 1000000):
|
|
54
|
+
continue
|
|
55
|
+
try:
|
|
56
|
+
record = read_record(raw, offset)
|
|
57
|
+
except UnsupportedFormat:
|
|
58
|
+
continue
|
|
59
|
+
if re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', record['name']):
|
|
60
|
+
records.append(record)
|
|
61
|
+
if len(records) != count:
|
|
62
|
+
raise UnsupportedFormat(f'Variable grid header says {count} records; parsed {len(records)}')
|
|
63
|
+
for key in ('handle', 'name'):
|
|
64
|
+
values = [str(r[key]).casefold() for r in records]
|
|
65
|
+
if len(set(values)) != len(values):
|
|
66
|
+
raise UnsupportedFormat(f'Duplicate variable grid {key}')
|
|
67
|
+
rows = [(r['group'], r['row']) for r in records]
|
|
68
|
+
if len(set(rows)) != len(rows):
|
|
69
|
+
raise UnsupportedFormat('Duplicate worksheet row within a group')
|
|
70
|
+
for left, right in zip(records, records[1:]):
|
|
71
|
+
if left['end'] > right['offset']:
|
|
72
|
+
raise UnsupportedFormat('Overlapping variable records')
|
|
73
|
+
end = records[-1]['end'] if records else 12
|
|
74
|
+
if end >= len(raw):
|
|
75
|
+
raise UnsupportedFormat('Native worksheet group trailer is missing')
|
|
76
|
+
return records
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def encode(record: dict, *, cells=None, handle=None, row=None) -> bytes:
|
|
80
|
+
fields = list(record['fields'])
|
|
81
|
+
if handle is not None: fields[0] = handle
|
|
82
|
+
if row is not None: fields[4] = row
|
|
83
|
+
out = struct.pack('<6I', *fields)
|
|
84
|
+
for cell in cells if cells is not None else record['cells']:
|
|
85
|
+
value = (cell + '\0').encode('utf-16le')
|
|
86
|
+
out += struct.pack('<I', len(value)) + value
|
|
87
|
+
return out + record['tail']
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find(records, name):
|
|
91
|
+
matches = [r for r in records if r['name'].casefold() == name.casefold()]
|
|
92
|
+
if len(matches) != 1:
|
|
93
|
+
raise NotFound(f'Expected one grid record for {name!r}; found {len(matches)}')
|
|
94
|
+
return matches[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def require_known_extent(records, record):
|
|
98
|
+
index = records.index(record)
|
|
99
|
+
if index + 1 < len(records) and records[index + 1]['offset'] != record['end']:
|
|
100
|
+
raise UnsupportedFormat('Selected native record has an unrecognized extension; refusing to synthesize or remove it')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add(raw, name, type_name, *, usage=1, address='', initial_value=None,
|
|
104
|
+
donor_name=None, donor_grid=None, group=None, row=None):
|
|
105
|
+
records = parse(raw)
|
|
106
|
+
if any(r['name'].casefold() == name.casefold() for r in records):
|
|
107
|
+
raise UnsupportedFormat(f'Variable {name} already exists in the worksheet')
|
|
108
|
+
donors = parse(donor_grid if donor_grid is not None else raw)
|
|
109
|
+
allowed = set(usage if isinstance(usage, (list, tuple)) else [usage])
|
|
110
|
+
# The type must match the donor. Measured on a real project, two records of different types
|
|
111
|
+
# looked interchangeable - six uint32 fields, a 16-byte tail, group 1 - but building one from
|
|
112
|
+
# a donor of another type produced a record the project could no longer parse, and MotionWorks
|
|
113
|
+
# had already rewritten it by the time it was read back. Treat the type as part of the layout
|
|
114
|
+
# contract; a donor of the wrong type is not a safe source.
|
|
115
|
+
candidates = [r for r in donors if r['usage'] in allowed
|
|
116
|
+
and r['type'].casefold() == type_name.casefold()
|
|
117
|
+
and bool(r['address']) == bool(address)
|
|
118
|
+
and (group is None or r['group'] == group)
|
|
119
|
+
and (donor_name is None or r['name'].casefold() == donor_name.casefold())]
|
|
120
|
+
if not candidates:
|
|
121
|
+
raise UnsupportedFormat('No compatible native donor: match type, usage, group and addressed/unaddressed layout')
|
|
122
|
+
# Different hidden flags/tails cannot be resolved by guessing. Equivalent donors are interchangeable.
|
|
123
|
+
shapes = {(r['fields'][1:4], r['tail']) for r in candidates}
|
|
124
|
+
if len(shapes) != 1 and donor_name is None:
|
|
125
|
+
raise UnsupportedFormat('Ambiguous native donors; pass donor explicitly')
|
|
126
|
+
donor = candidates[0]
|
|
127
|
+
require_known_extent(donors, donor)
|
|
128
|
+
if donor_grid is not None and donor['group'] != 1:
|
|
129
|
+
raise UnsupportedFormat('Cross-worksheet donors require the Default group')
|
|
130
|
+
handle = max([struct.unpack_from('<I', raw, 4)[0]] + [r['handle'] for r in records]) + 1
|
|
131
|
+
# Rows are native worksheet identifiers, NOT declaration source line numbers.
|
|
132
|
+
new_row = max([0] + [r['row'] for r in records if r['group'] == donor['group']]) + 1
|
|
133
|
+
if row is not None:
|
|
134
|
+
new_row = row
|
|
135
|
+
if (donor['group'], new_row) in {(r['group'], r['row']) for r in records}:
|
|
136
|
+
raise UnsupportedFormat('Requested worksheet row already exists')
|
|
137
|
+
cells = [type_name, address or '', donor['initial_value'] if initial_value is None else initial_value, name]
|
|
138
|
+
record = encode(donor, cells=cells, handle=handle, row=new_row)
|
|
139
|
+
end = records[-1]['end'] if records else 12
|
|
140
|
+
out = bytearray(raw[:end] + record + raw[end:])
|
|
141
|
+
struct.pack_into('<II', out, 4, handle, len(records) + 1)
|
|
142
|
+
parse(bytes(out))
|
|
143
|
+
return bytes(out), handle
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def edit(raw, name, *, new_name=None, type_name=None, initial_value=None, address=None,
|
|
147
|
+
donor_name=None):
|
|
148
|
+
records = parse(raw)
|
|
149
|
+
current = find(records, name)
|
|
150
|
+
require_known_extent(records, current)
|
|
151
|
+
donor = current
|
|
152
|
+
wanted_type = type_name if type_name is not None else current['type']
|
|
153
|
+
wanted_address = address if address is not None else current['address']
|
|
154
|
+
if wanted_type.casefold() != current['type'].casefold() or bool(wanted_address) != bool(current['address']):
|
|
155
|
+
matches = [r for r in records if r['type'].casefold() == wanted_type.casefold()
|
|
156
|
+
and r['usage'] == current['usage'] and r['group'] == current['group']
|
|
157
|
+
and bool(r['address']) == bool(wanted_address)
|
|
158
|
+
and (donor_name is None or r['name'].casefold() == donor_name.casefold())]
|
|
159
|
+
if not matches or (len({r['tail'] for r in matches}) != 1 and donor_name is None):
|
|
160
|
+
raise UnsupportedFormat('Type/address-layout change requires an unambiguous compatible native donor')
|
|
161
|
+
donor = matches[0]
|
|
162
|
+
require_known_extent(records, donor)
|
|
163
|
+
cells = [wanted_type, wanted_address, current['initial_value'] if initial_value is None else initial_value,
|
|
164
|
+
current['name'] if new_name is None else new_name]
|
|
165
|
+
replacement = encode(donor, cells=cells, handle=current['handle'], row=current['row'])
|
|
166
|
+
# Preserve the edited variable's translation ID rather than the type donor's.
|
|
167
|
+
replacement = bytearray(replacement)
|
|
168
|
+
replacement[-8:-4] = current['tail'][8:12]
|
|
169
|
+
replacement = bytes(replacement)
|
|
170
|
+
out = raw[:current['offset']] + replacement + raw[current['end']:]
|
|
171
|
+
parse(out)
|
|
172
|
+
return out, dict(handle=current['handle'], row=current['row'], **{'from': name, 'to': cells[3]})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def delete(raw, name):
|
|
176
|
+
records = parse(raw)
|
|
177
|
+
record = find(records, name)
|
|
178
|
+
require_known_extent(records, record)
|
|
179
|
+
out = bytearray(raw[:record['offset']] + raw[record['end']:])
|
|
180
|
+
struct.pack_into('<I', out, 8, len(records) - 1)
|
|
181
|
+
parse(bytes(out))
|
|
182
|
+
return bytes(out), dict(handle=record['handle'], row=record['row'], removed=name)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def pair_findings(text, raw):
|
|
186
|
+
"""Report every declaration/worksheet inconsistency instead of raising on the first.
|
|
187
|
+
|
|
188
|
+
A project may already carry a record the compiler and the IDE both accept while the two
|
|
189
|
+
redundant stores disagree, and a vendor-authored project does exactly that. Collecting the
|
|
190
|
+
findings lets a caller keep working on an unrelated variable while still proving that the
|
|
191
|
+
edit introduces no new disagreement of its own.
|
|
192
|
+
"""
|
|
193
|
+
from .variables import parse_declarations
|
|
194
|
+
findings = []
|
|
195
|
+
table = parse_declarations(text)
|
|
196
|
+
if table.warnings or table.duplicates():
|
|
197
|
+
# The declaration table itself is untrustworthy, so every comparison below would be noise.
|
|
198
|
+
return ['Declaration parser warnings: ' + '; '.join(table.warnings)]
|
|
199
|
+
records = parse(raw)
|
|
200
|
+
expected = {v.name.casefold(): v for v in table.variables}
|
|
201
|
+
actual = {r['name'].casefold(): r for r in records}
|
|
202
|
+
if expected.keys() != actual.keys():
|
|
203
|
+
return [f'Declaration/grid mismatch: text-only={sorted(expected.keys()-actual.keys())}, grid-only={sorted(actual.keys()-expected.keys())}']
|
|
204
|
+
for name, variable in expected.items():
|
|
205
|
+
record = actual[name]
|
|
206
|
+
if variable.type_name.casefold() != record['type'].casefold() or (variable.address or '').casefold() != record['address'].casefold():
|
|
207
|
+
findings.append(f'Declaration/grid type or address differs for {variable.name}')
|
|
208
|
+
continue
|
|
209
|
+
expected_initial = variable.initial_value.strip().upper() if variable.initial_value is not None else None
|
|
210
|
+
actual_initial = record['initial_value'].strip().upper()
|
|
211
|
+
primitive_defaults = {'BOOL': {'FALSE', '0'}, 'INT': {'0'}, 'DINT': {'0'}, 'SINT': {'0'},
|
|
212
|
+
'UINT': {'0'}, 'UDINT': {'0'}, 'USINT': {'0'}, 'REAL': {'0', '0.0'}, 'LREAL': {'0', '0.0'}}
|
|
213
|
+
empty_default = not actual_initial and expected_initial in primitive_defaults.get(variable.type_name.upper(), set())
|
|
214
|
+
if expected_initial is not None and expected_initial != actual_initial and not empty_default:
|
|
215
|
+
findings.append(f'Declaration/grid initial value differs for {variable.name}')
|
|
216
|
+
allowed = {'VAR_EXTERNAL': (5,), 'VAR_GLOBAL': (6, 22), 'VAR': (1, 0x40001)}.get(variable.section)
|
|
217
|
+
if allowed and record['usage'] not in allowed:
|
|
218
|
+
findings.append(f'Declaration/grid usage differs for {variable.name}')
|
|
219
|
+
return findings
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def validate_pair(text, raw):
|
|
223
|
+
findings = pair_findings(text, raw)
|
|
224
|
+
if findings:
|
|
225
|
+
raise UnsupportedFormat(findings[0])
|
|
226
|
+
records = parse(raw)
|
|
227
|
+
return {'ok': True, 'variables': len(records), 'trailer_bytes': len(raw) - (records[-1]['end'] if records else 12)}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Detect and close the MotionWorks IEC IDE.
|
|
2
|
+
|
|
3
|
+
Writing to a project while the IDE has it open is unsafe: the IDE holds project state
|
|
4
|
+
in memory and rewrites whole files on save, so an external edit is either discarded or
|
|
5
|
+
leaves an internally inconsistent project. Every Tier 2 write therefore gates on the
|
|
6
|
+
IDE being closed -- and, when asked, closes it rather than merely refusing.
|
|
7
|
+
|
|
8
|
+
**Discovery is by window ownership, chosen after two other approaches failed
|
|
9
|
+
measurement here:**
|
|
10
|
+
|
|
11
|
+
1. ``CreateToolhelp32Snapshot``, the obvious ctypes approach, enumerates only 50 of
|
|
12
|
+
~460 processes and **silently omits ``Mwt.exe``**. A safety gate built on it says
|
|
13
|
+
"the IDE is closed" while the IDE is running, which is the worst failure it can
|
|
14
|
+
have: a write would then proceed under an open IDE.
|
|
15
|
+
2. Enumerating from a **child process** does not help. This Python process runs in a
|
|
16
|
+
restricted Job object whose view is truncated, and children inherit it -- a
|
|
17
|
+
PowerShell script spawned from Python sees the same ~52 processes, while the same
|
|
18
|
+
script run by hand sees all ~460.
|
|
19
|
+
|
|
20
|
+
In-process ``EnumWindows`` is not restricted, and the IDE is a GUI application, so it
|
|
21
|
+
always owns windows. Every candidate PID's image name is then **verified** with
|
|
22
|
+
``QueryFullProcessImageNameW`` before it is reported or closed, because acting on an
|
|
23
|
+
unverified PID would mean killing the wrong process.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
#: Executable names that indicate the IDE. Only these block writing.
|
|
32
|
+
MAIN_PROCESSES = ("mwt", "mwt.exe", "mwiec", "motionworks")
|
|
33
|
+
#: Helper processes that can hold the same project files open. They are closed
|
|
34
|
+
#: alongside the IDE and reported, but a bare write gate does not fail on them.
|
|
35
|
+
HELPER_PROCESSES = (
|
|
36
|
+
"mwcameditor",
|
|
37
|
+
"yaskawa.plottool",
|
|
38
|
+
"plc_simulator",
|
|
39
|
+
# Yaskawa's licence verifier. It owns the "Use Trial" dialog, keeps the
|
|
40
|
+
# single-instance lock alive while it waits for an answer, and can outlive
|
|
41
|
+
# Mwt.exe -- which exits while the dialog is still up. Closing MotionWorks
|
|
42
|
+
# must therefore close this too, or the next launch is blocked.
|
|
43
|
+
"mwctverify",
|
|
44
|
+
)
|
|
45
|
+
#: Everything the module will detect or close.
|
|
46
|
+
ALL_PROCESSES = MAIN_PROCESSES + HELPER_PROCESSES
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _window_owner_pids() -> set:
|
|
50
|
+
"""PIDs that own a top-level window, found in-process via ``EnumWindows``."""
|
|
51
|
+
if sys.platform != "win32":
|
|
52
|
+
return set()
|
|
53
|
+
import ctypes
|
|
54
|
+
from ctypes import wintypes
|
|
55
|
+
|
|
56
|
+
user32 = ctypes.windll.user32
|
|
57
|
+
pids = set()
|
|
58
|
+
callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
59
|
+
get_pid = user32.GetWindowThreadProcessId
|
|
60
|
+
get_pid.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)]
|
|
61
|
+
|
|
62
|
+
def visit(hwnd, _lparam):
|
|
63
|
+
pid = wintypes.DWORD()
|
|
64
|
+
get_pid(hwnd, ctypes.byref(pid))
|
|
65
|
+
if pid.value:
|
|
66
|
+
pids.add(pid.value)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
if not user32.EnumWindows(callback_type(visit), 0):
|
|
71
|
+
raise RuntimeError('Could not enumerate IDE windows; offline write refused')
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
raise RuntimeError('Could not verify that the IDE is closed') from exc
|
|
74
|
+
return pids
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _image_name(pid: int):
|
|
78
|
+
"""The executable name of ``pid``, or None if it cannot be queried."""
|
|
79
|
+
if sys.platform != "win32":
|
|
80
|
+
return None
|
|
81
|
+
import ctypes
|
|
82
|
+
from ctypes import wintypes
|
|
83
|
+
|
|
84
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
85
|
+
kernel32 = ctypes.windll.kernel32
|
|
86
|
+
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
87
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
88
|
+
kernel32.QueryFullProcessImageNameW.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)]
|
|
89
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
90
|
+
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
91
|
+
if not handle:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
size = wintypes.DWORD(1024)
|
|
95
|
+
buffer = ctypes.create_unicode_buffer(size.value)
|
|
96
|
+
ok = kernel32.QueryFullProcessImageNameW(handle, 0, buffer, ctypes.byref(size))
|
|
97
|
+
if not ok:
|
|
98
|
+
return None
|
|
99
|
+
# Strip the .exe suffix: the process-name lists here are written without it,
|
|
100
|
+
# and leaving it on silently breaks detection. Measured: "mwctVerify.exe"
|
|
101
|
+
# failed to match "mwctverify" in HELPER_PROCESSES, so the licence verifier
|
|
102
|
+
# went unreported even though it owned the window being looked for.
|
|
103
|
+
name = buffer.value.rsplit(chr(92), 1)[-1]
|
|
104
|
+
if name.casefold().endswith(".exe"):
|
|
105
|
+
name = name[:-4]
|
|
106
|
+
return name
|
|
107
|
+
finally:
|
|
108
|
+
kernel32.CloseHandle(handle)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _terminate(pid: int) -> bool:
|
|
112
|
+
"""Terminate a process by PID; True if the call was accepted."""
|
|
113
|
+
if sys.platform != "win32":
|
|
114
|
+
return False
|
|
115
|
+
import ctypes
|
|
116
|
+
|
|
117
|
+
PROCESS_TERMINATE = 0x0001
|
|
118
|
+
SYNCHRONIZE = 0x00100000
|
|
119
|
+
kernel32 = ctypes.windll.kernel32
|
|
120
|
+
handle = kernel32.OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, False, pid)
|
|
121
|
+
if not handle:
|
|
122
|
+
return False
|
|
123
|
+
try:
|
|
124
|
+
kernel32.TerminateProcess(handle, 1)
|
|
125
|
+
# Wait, so a caller never proceeds while the process still holds files open.
|
|
126
|
+
kernel32.WaitForSingleObject(handle, 10_000)
|
|
127
|
+
return True
|
|
128
|
+
finally:
|
|
129
|
+
kernel32.CloseHandle(handle)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
#: The PowerShell fallback helper, written to a file and invoked with ``-File``:
|
|
133
|
+
#: ``-Command`` with inline script text is mangled by this shell's quoting and
|
|
134
|
+
#: silently returns nothing. Only useful to callers *outside* the restricted job.
|
|
135
|
+
_HELPER = r"""
|
|
136
|
+
param([string]$Out = '')
|
|
137
|
+
|
|
138
|
+
$names = @('Mwt', 'Mwcameditor', 'Yaskawa.PlotTool', 'plc_simulator')
|
|
139
|
+
$found = @(Get-Process -ErrorAction SilentlyContinue |
|
|
140
|
+
Where-Object { $names -contains $_.ProcessName })
|
|
141
|
+
$lines = New-Object System.Collections.Generic.List[string]
|
|
142
|
+
foreach ($p in $found) { $lines.Add(("running`t{0}`t{1}" -f $p.Id, $p.ProcessName)) }
|
|
143
|
+
$lines.Add("done`t0`t$($found.Count)")
|
|
144
|
+
if ($Out) { Set-Content -LiteralPath $Out -Value $lines -Encoding ascii } else { $lines }
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _run_helper(timeout_s: float = 20.0):
|
|
149
|
+
"""Run the PowerShell helper; returns ``(kind, pid, name)`` rows."""
|
|
150
|
+
import subprocess
|
|
151
|
+
import tempfile
|
|
152
|
+
|
|
153
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
154
|
+
script = Path(tmp) / "ide_helper.ps1"
|
|
155
|
+
listing = Path(tmp) / "ide_helper.out"
|
|
156
|
+
script.write_text(_HELPER, encoding="ascii")
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run(
|
|
159
|
+
[
|
|
160
|
+
"powershell.exe",
|
|
161
|
+
"-NoLogo",
|
|
162
|
+
"-NoProfile",
|
|
163
|
+
"-NonInteractive",
|
|
164
|
+
"-ExecutionPolicy",
|
|
165
|
+
"Bypass",
|
|
166
|
+
"-File",
|
|
167
|
+
str(script),
|
|
168
|
+
"-Out",
|
|
169
|
+
str(listing),
|
|
170
|
+
],
|
|
171
|
+
stdout=subprocess.DEVNULL,
|
|
172
|
+
stderr=subprocess.DEVNULL,
|
|
173
|
+
timeout=timeout_s + 120,
|
|
174
|
+
creationflags=0x08000000, # CREATE_NO_WINDOW
|
|
175
|
+
)
|
|
176
|
+
except Exception: # noqa: BLE001
|
|
177
|
+
return []
|
|
178
|
+
try:
|
|
179
|
+
text = listing.read_text(encoding="ascii", errors="replace")
|
|
180
|
+
except OSError:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
rows = []
|
|
184
|
+
for line in text.splitlines():
|
|
185
|
+
parts = line.split("\t")
|
|
186
|
+
if len(parts) == 3:
|
|
187
|
+
rows.append((parts[0], parts[1], parts[2]))
|
|
188
|
+
return rows
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def motionworks_running(include_helpers: bool = True):
|
|
192
|
+
"""Return ``(pid, exe_name)`` for running MotionWorks-related processes.
|
|
193
|
+
|
|
194
|
+
An empty list means it is safe to write. Only :data:`MAIN_PROCESSES` block
|
|
195
|
+
writing; the helpers are informational.
|
|
196
|
+
"""
|
|
197
|
+
if sys.platform != "win32":
|
|
198
|
+
return []
|
|
199
|
+
wanted = MAIN_PROCESSES + HELPER_PROCESSES if include_helpers else MAIN_PROCESSES
|
|
200
|
+
found = []
|
|
201
|
+
for pid in sorted(_window_owner_pids()):
|
|
202
|
+
# Verify before reporting: the caller may close these PIDs.
|
|
203
|
+
name = _image_name(pid)
|
|
204
|
+
if name and name.casefold() in wanted:
|
|
205
|
+
found.append((pid, name))
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ide_blocks_writes():
|
|
210
|
+
"""Return whether the IDE blocks writing, plus the offending processes."""
|
|
211
|
+
procs = motionworks_running(include_helpers=False)
|
|
212
|
+
return bool(procs), procs
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def close_motionworks(timeout: float = 15.0):
|
|
216
|
+
"""Close every running MotionWorks instance and verify it is gone.
|
|
217
|
+
|
|
218
|
+
The IDE is asked to quit through COM first, when pywin32 is installed and the
|
|
219
|
+
automation server is already running, so it can flush its own state. Anything
|
|
220
|
+
still alive is then terminated by PID. The list is **re-read** afterwards, so
|
|
221
|
+
``remaining`` is what is actually still running rather than an assumption -- a
|
|
222
|
+
caller must not write while it is non-empty.
|
|
223
|
+
"""
|
|
224
|
+
import time
|
|
225
|
+
|
|
226
|
+
if sys.platform != "win32":
|
|
227
|
+
return {"before": [], "closed": [], "remaining": [], "ok": True}
|
|
228
|
+
|
|
229
|
+
before = motionworks_running(include_helpers=True)
|
|
230
|
+
|
|
231
|
+
# Ask nicely, if the automation server is reachable. 32-bit pywin32 is an
|
|
232
|
+
# optional extra, so this is skipped rather than failing when absent.
|
|
233
|
+
if any(name.casefold() in MAIN_PROCESSES for _, name in before):
|
|
234
|
+
try:
|
|
235
|
+
import win32com.client # type: ignore[import-not-found]
|
|
236
|
+
|
|
237
|
+
app = win32com.client.GetActiveObject("Ade.Application.550")
|
|
238
|
+
app.Quit(0)
|
|
239
|
+
time.sleep(1.5)
|
|
240
|
+
except Exception: # noqa: BLE001 - not installed, or nothing to quit
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
closed = []
|
|
244
|
+
deadline = time.monotonic() + timeout
|
|
245
|
+
while time.monotonic() < deadline:
|
|
246
|
+
still = motionworks_running(include_helpers=True)
|
|
247
|
+
if not still:
|
|
248
|
+
break
|
|
249
|
+
for pid, name in still:
|
|
250
|
+
if _terminate(pid):
|
|
251
|
+
closed.append((pid, name))
|
|
252
|
+
time.sleep(0.4)
|
|
253
|
+
|
|
254
|
+
remaining = motionworks_running(include_helpers=True)
|
|
255
|
+
return {
|
|
256
|
+
"before": before,
|
|
257
|
+
"closed": closed,
|
|
258
|
+
"remaining": remaining,
|
|
259
|
+
"ok": not remaining,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def auto_close_enabled() -> bool:
|
|
264
|
+
"""Whether write tools should close the IDE themselves.
|
|
265
|
+
|
|
266
|
+
Controlled by ``MOTIONWORKS_MCP_CLOSE_IDE``: ``1``/``true``/``yes``/``on`` means
|
|
267
|
+
a write closes a running IDE and proceeds rather than refusing. Off by default,
|
|
268
|
+
because silently closing an application the user may be working in is not a
|
|
269
|
+
decision to make on their behalf.
|
|
270
|
+
"""
|
|
271
|
+
import os
|
|
272
|
+
|
|
273
|
+
value = os.environ.get("MOTIONWORKS_MCP_CLOSE_IDE", "")
|
|
274
|
+
return value.strip().casefold() in {"1", "true", "yes", "on"}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def ensure_ide_closed(auto_close: bool | None = None, timeout: float = 15.0):
|
|
278
|
+
"""Raise :class:`IdeRunning` unless the IDE is closed, closing it if configured.
|
|
279
|
+
|
|
280
|
+
Every write path calls this instead of inspecting processes itself, so the
|
|
281
|
+
close-or-refuse policy lives in exactly one place.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
auto_close: Override the environment setting; ``None`` uses
|
|
285
|
+
:func:`auto_close_enabled`.
|
|
286
|
+
timeout: How long to wait for the IDE to exit when closing it.
|
|
287
|
+
"""
|
|
288
|
+
from .errors import IdeRunning
|
|
289
|
+
|
|
290
|
+
if sys.platform != "win32":
|
|
291
|
+
return
|
|
292
|
+
# Project identity can only be verified by the bridge close tool.
|
|
293
|
+
auto_close = False
|
|
294
|
+
procs = require_ide_closed(auto_close=auto_close, timeout=timeout)
|
|
295
|
+
if procs:
|
|
296
|
+
described = ", ".join(f"{name} (pid {pid})" for pid, name in procs)
|
|
297
|
+
raise IdeRunning(
|
|
298
|
+
f"MotionWorks is running ({described}), so nothing was written. Close "
|
|
299
|
+
f"MotionWorks IEC through the guarded mw_ide_close tool and retry."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def require_ide_closed(auto_close: bool = False, timeout: float = 15.0):
|
|
304
|
+
"""Ensure the IDE is closed, closing it first when asked.
|
|
305
|
+
|
|
306
|
+
Returns the processes *still* running (empty means safe to write). With
|
|
307
|
+
``auto_close`` the IDE is shut down when found; without it this only reports, so
|
|
308
|
+
a caller can refuse and tell the user to close it.
|
|
309
|
+
"""
|
|
310
|
+
blocking, procs = ide_blocks_writes()
|
|
311
|
+
if not blocking:
|
|
312
|
+
return []
|
|
313
|
+
if not auto_close:
|
|
314
|
+
return procs
|
|
315
|
+
close_motionworks(timeout=timeout)
|
|
316
|
+
_, procs = ide_blocks_writes()
|
|
317
|
+
return procs
|