@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.
Files changed (161) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +37 -26
  3. package/cordis.patch.yml +7 -6
  4. package/entries/config.js +62 -0
  5. package/entries/ftview.js +5 -4
  6. package/entries/l5x.js +61 -13
  7. package/entries/mwi.js +26 -6
  8. package/entries/optix.js +105 -0
  9. package/lib/client.js +335 -6
  10. package/package.json +11 -10
  11. package/vendor/l5x/README.md +27 -5
  12. package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
  13. package/vendor/mwi/README.md +39 -18
  14. package/vendor/mwi/SKILL.md +26 -20
  15. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -2763
  16. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
  17. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
  18. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
  19. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
  20. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
  21. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
  22. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
  23. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
  24. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
  25. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
  26. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
  27. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +128 -128
  28. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
  29. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
  30. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
  31. package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
  32. package/vendor/mwi/lib/core/code-engine.js +18 -1
  33. package/vendor/mwi/lib/edit.d.ts +1 -1
  34. package/vendor/mwi/lib/edit.js +1 -1
  35. package/vendor/mwi/lib/read/service.js +8 -1
  36. package/vendor/mwi/lib/session/backup.d.ts +55 -0
  37. package/vendor/mwi/lib/session/backup.js +232 -0
  38. package/vendor/mwi/lib/session/project.d.ts +4 -1
  39. package/vendor/mwi/lib/session/project.js +13 -2
  40. package/vendor/mwi/lib/session/service.d.ts +13 -6
  41. package/vendor/mwi/lib/session/service.js +127 -36
  42. package/vendor/mwi/lib/session/workspace.d.ts +10 -0
  43. package/vendor/mwi/lib/session/workspace.js +53 -3
  44. package/vendor/mwi/lib/skill.d.ts +2 -2
  45. package/vendor/mwi/lib/skill.js +2 -2
  46. package/vendor/mwi/lib/tools/edit.d.ts +3 -3
  47. package/vendor/mwi/lib/tools/edit.js +14 -15
  48. package/vendor/mwi/lib/tools/read.js +1 -1
  49. package/vendor/mwi/lib/tools/session.js +13 -9
  50. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
  51. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
  52. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
  53. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
  54. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  55. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
  56. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
  57. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
  58. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
  59. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
  60. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  61. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  62. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
  63. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
  64. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  65. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
  66. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
  67. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
  68. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
  69. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
  70. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
  71. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  72. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
  73. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
  74. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  75. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
  76. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
  77. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
  78. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
  79. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
  80. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
  81. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  82. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
  83. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
  84. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
  85. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
  86. package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
  87. package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  88. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
  89. package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
  90. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
  91. package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
  92. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
  93. package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  94. package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  95. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
  96. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
  97. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  98. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
  99. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
  100. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
  101. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
  102. package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
  103. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
  104. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  105. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
  106. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
  107. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
  108. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
  109. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
  110. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
  111. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
  112. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
  113. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
  114. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  115. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
  116. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
  117. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
  118. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
  119. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
  120. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
  121. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
  122. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
  123. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
  124. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
  125. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
  126. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
  127. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
  128. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
  129. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
  130. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
  131. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
  132. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
  133. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
  134. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
  135. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
  136. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  137. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
  138. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
  139. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
  140. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  142. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  143. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  144. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  145. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  146. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  147. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  148. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  149. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  150. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  151. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  152. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  153. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  154. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  155. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  156. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  157. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  158. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  159. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  160. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  161. 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