@aryacode/dsh-engineering 0.4.6 → 0.4.20
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 +199 -0
- package/README.md +37 -26
- package/cordis.patch.yml +7 -6
- package/entries/config.js +62 -0
- package/entries/ftview.js +5 -4
- package/entries/l5x.js +61 -13
- package/entries/mwi.js +26 -6
- package/entries/optix.js +105 -0
- package/lib/client.js +335 -6
- package/package.json +11 -10
- package/vendor/l5x/README.md +27 -5
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
- package/vendor/mwi/README.md +39 -18
- package/vendor/mwi/SKILL.md +26 -20
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -2763
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +128 -128
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
- package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
- package/vendor/mwi/lib/core/code-engine.js +18 -1
- package/vendor/mwi/lib/edit.d.ts +1 -1
- package/vendor/mwi/lib/edit.js +1 -1
- package/vendor/mwi/lib/read/service.js +8 -1
- package/vendor/mwi/lib/session/backup.d.ts +55 -0
- package/vendor/mwi/lib/session/backup.js +232 -0
- package/vendor/mwi/lib/session/project.d.ts +4 -1
- package/vendor/mwi/lib/session/project.js +13 -2
- package/vendor/mwi/lib/session/service.d.ts +13 -6
- package/vendor/mwi/lib/session/service.js +127 -36
- package/vendor/mwi/lib/session/workspace.d.ts +10 -0
- package/vendor/mwi/lib/session/workspace.js +53 -3
- package/vendor/mwi/lib/skill.d.ts +2 -2
- package/vendor/mwi/lib/skill.js +2 -2
- package/vendor/mwi/lib/tools/edit.d.ts +3 -3
- package/vendor/mwi/lib/tools/edit.js +14 -15
- package/vendor/mwi/lib/tools/read.js +1 -1
- package/vendor/mwi/lib/tools/session.js +13 -9
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
- 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
|
@@ -1,317 +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:
|
|
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
70
|
if not user32.EnumWindows(callback_type(visit), 0):
|
|
71
71
|
raise RuntimeError('Could not enumerate IDE windows; offline write refused')
|
|
72
72
|
except Exception as exc:
|
|
73
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
|
|
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
|