@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.
Files changed (284) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +21 -0
  3. package/README.md +230 -2
  4. package/cordis.patch.yml +123 -0
  5. package/lib/client.js +3448 -0
  6. package/lib/index.d.ts +2 -0
  7. package/lib/index.js +9 -0
  8. package/locale/en.json +6 -0
  9. package/locale/zh.json +6 -0
  10. package/package.json +94 -3
  11. package/vendor/ftview/README.md +55 -0
  12. package/vendor/ftview/SKILL.md +74 -0
  13. package/vendor/ftview/lib/checks.d.ts +27 -0
  14. package/vendor/ftview/lib/checks.js +39 -0
  15. package/vendor/ftview/lib/core.d.ts +208 -0
  16. package/vendor/ftview/lib/core.js +160 -0
  17. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  18. package/vendor/ftview/lib/edit/drafts.js +84 -0
  19. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  20. package/vendor/ftview/lib/edit/operations.js +126 -0
  21. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  22. package/vendor/ftview/lib/edit/prepare.js +133 -0
  23. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  24. package/vendor/ftview/lib/edit/tools.js +198 -0
  25. package/vendor/ftview/lib/edit.d.ts +30 -0
  26. package/vendor/ftview/lib/edit.js +38 -0
  27. package/vendor/ftview/lib/index.d.ts +31 -0
  28. package/vendor/ftview/lib/index.js +36 -0
  29. package/vendor/ftview/lib/model.d.ts +80 -0
  30. package/vendor/ftview/lib/model.js +267 -0
  31. package/vendor/ftview/lib/session.d.ts +48 -0
  32. package/vendor/ftview/lib/session.js +98 -0
  33. package/vendor/ftview/lib/skill.d.ts +29 -0
  34. package/vendor/ftview/lib/skill.js +43 -0
  35. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  36. package/vendor/ftview/lib/studio/discover.js +87 -0
  37. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  38. package/vendor/ftview/lib/studio/preflight.js +64 -0
  39. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  40. package/vendor/ftview/lib/studio/tools.js +83 -0
  41. package/vendor/ftview/lib/studio.d.ts +30 -0
  42. package/vendor/ftview/lib/studio.js +37 -0
  43. package/vendor/ftview/lib/tools.d.ts +14 -0
  44. package/vendor/ftview/lib/tools.js +202 -0
  45. package/vendor/l5x/README.md +62 -0
  46. package/vendor/l5x/SKILL.md +81 -0
  47. package/vendor/l5x/lib/approvals.d.ts +50 -0
  48. package/vendor/l5x/lib/approvals.js +93 -0
  49. package/vendor/l5x/lib/client.js +412 -0
  50. package/vendor/l5x/lib/client.js.map +1 -0
  51. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  52. package/vendor/l5x/lib/core/errors.js +48 -0
  53. package/vendor/l5x/lib/core/params.d.ts +49 -0
  54. package/vendor/l5x/lib/core/params.js +39 -0
  55. package/vendor/l5x/lib/core/result.d.ts +46 -0
  56. package/vendor/l5x/lib/core/result.js +21 -0
  57. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  58. package/vendor/l5x/lib/core/tool.js +19 -0
  59. package/vendor/l5x/lib/drafts.d.ts +66 -0
  60. package/vendor/l5x/lib/drafts.js +98 -0
  61. package/vendor/l5x/lib/editing.d.ts +30 -0
  62. package/vendor/l5x/lib/editing.js +42 -0
  63. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  64. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  65. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  66. package/vendor/l5x/lib/engine/analysis.js +258 -0
  67. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  68. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  69. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  70. package/vendor/l5x/lib/engine/change-set.js +446 -0
  71. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  72. package/vendor/l5x/lib/engine/configuration.js +42 -0
  73. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  74. package/vendor/l5x/lib/engine/details.js +173 -0
  75. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  76. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  77. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  78. package/vendor/l5x/lib/engine/download-review.js +63 -0
  79. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  80. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  81. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  82. package/vendor/l5x/lib/engine/observations.js +67 -0
  83. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  84. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  85. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  86. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  87. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  88. package/vendor/l5x/lib/engine/review.js +123 -0
  89. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  90. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  91. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  92. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  93. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  94. package/vendor/l5x/lib/engine/usage.js +92 -0
  95. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  96. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  97. package/vendor/l5x/lib/fs.d.ts +17 -0
  98. package/vendor/l5x/lib/fs.js +36 -0
  99. package/vendor/l5x/lib/index.d.ts +36 -0
  100. package/vendor/l5x/lib/index.js +42 -0
  101. package/vendor/l5x/lib/live.d.ts +48 -0
  102. package/vendor/l5x/lib/live.js +48 -0
  103. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  104. package/vendor/l5x/lib/model/aois.js +112 -0
  105. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  106. package/vendor/l5x/lib/model/errors.js +18 -0
  107. package/vendor/l5x/lib/model/index.d.ts +20 -0
  108. package/vendor/l5x/lib/model/index.js +19 -0
  109. package/vendor/l5x/lib/model/module.d.ts +37 -0
  110. package/vendor/l5x/lib/model/module.js +54 -0
  111. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  112. package/vendor/l5x/lib/model/modules.js +55 -0
  113. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  114. package/vendor/l5x/lib/model/parse.js +116 -0
  115. package/vendor/l5x/lib/model/project.d.ts +15 -0
  116. package/vendor/l5x/lib/model/project.js +31 -0
  117. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  118. package/vendor/l5x/lib/model/routines.js +183 -0
  119. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  120. package/vendor/l5x/lib/model/tags.js +119 -0
  121. package/vendor/l5x/lib/model/types.d.ts +304 -0
  122. package/vendor/l5x/lib/model/types.js +12 -0
  123. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  124. package/vendor/l5x/lib/model/udts.js +81 -0
  125. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  126. package/vendor/l5x/lib/model/xml.js +291 -0
  127. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  128. package/vendor/l5x/lib/model/xref.js +154 -0
  129. package/vendor/l5x/lib/session.d.ts +119 -0
  130. package/vendor/l5x/lib/session.js +236 -0
  131. package/vendor/l5x/lib/skill.d.ts +30 -0
  132. package/vendor/l5x/lib/skill.js +44 -0
  133. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  134. package/vendor/l5x/lib/studio-state.js +79 -0
  135. package/vendor/l5x/lib/studio.d.ts +29 -0
  136. package/vendor/l5x/lib/studio.js +40 -0
  137. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  138. package/vendor/l5x/lib/tools/checks.js +140 -0
  139. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  140. package/vendor/l5x/lib/tools/common.js +84 -0
  141. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  142. package/vendor/l5x/lib/tools/controller.js +170 -0
  143. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  144. package/vendor/l5x/lib/tools/drafts.js +270 -0
  145. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  146. package/vendor/l5x/lib/tools/live.js +175 -0
  147. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  148. package/vendor/l5x/lib/tools/read.js +245 -0
  149. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  150. package/vendor/l5x/lib/tools/references.js +269 -0
  151. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  152. package/vendor/l5x/lib/tools/studio.js +193 -0
  153. package/vendor/l5x/poll-worker.mjs +77 -0
  154. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  155. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  156. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  157. package/vendor/mwi/README.md +60 -0
  158. package/vendor/mwi/SKILL.md +87 -0
  159. package/vendor/mwi/bridge/README.md +8 -0
  160. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  161. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  162. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  163. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  164. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  165. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  229. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  230. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  231. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  232. package/vendor/mwi/code/mw_code.py +1258 -0
  233. package/vendor/mwi/legacy/edit-session.js +50 -0
  234. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  235. package/vendor/mwi/legacy/native-code.js +83 -0
  236. package/vendor/mwi/legacy/native-groups.js +75 -0
  237. package/vendor/mwi/legacy/native-structure.js +143 -0
  238. package/vendor/mwi/legacy/native-variables.js +77 -0
  239. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  240. package/vendor/mwi/legacy/pou-package.js +66 -0
  241. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  242. package/vendor/mwi/lib/core/bridge.js +53 -0
  243. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  244. package/vendor/mwi/lib/core/code-engine.js +109 -0
  245. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  246. package/vendor/mwi/lib/core/errors.js +47 -0
  247. package/vendor/mwi/lib/core/params.d.ts +52 -0
  248. package/vendor/mwi/lib/core/params.js +46 -0
  249. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  250. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  251. package/vendor/mwi/lib/core/result.d.ts +45 -0
  252. package/vendor/mwi/lib/core/result.js +20 -0
  253. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  254. package/vendor/mwi/lib/core/tool.js +55 -0
  255. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  256. package/vendor/mwi/lib/edit/compare.js +70 -0
  257. package/vendor/mwi/lib/edit.d.ts +20 -0
  258. package/vendor/mwi/lib/edit.js +32 -0
  259. package/vendor/mwi/lib/index.d.ts +21 -0
  260. package/vendor/mwi/lib/index.js +33 -0
  261. package/vendor/mwi/lib/read/service.d.ts +117 -0
  262. package/vendor/mwi/lib/read/service.js +229 -0
  263. package/vendor/mwi/lib/read.d.ts +21 -0
  264. package/vendor/mwi/lib/read.js +33 -0
  265. package/vendor/mwi/lib/session/project.d.ts +34 -0
  266. package/vendor/mwi/lib/session/project.js +54 -0
  267. package/vendor/mwi/lib/session/service.d.ts +167 -0
  268. package/vendor/mwi/lib/session/service.js +566 -0
  269. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  270. package/vendor/mwi/lib/session/stage.js +187 -0
  271. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  272. package/vendor/mwi/lib/session/workspace.js +90 -0
  273. package/vendor/mwi/lib/skill.d.ts +36 -0
  274. package/vendor/mwi/lib/skill.js +56 -0
  275. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  276. package/vendor/mwi/lib/tools/edit.js +286 -0
  277. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  278. package/vendor/mwi/lib/tools/read.js +161 -0
  279. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  280. package/vendor/mwi/lib/tools/session.js +166 -0
  281. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  282. package/vendor/mwi/lib/tools/verify.js +85 -0
  283. package/vendor/mwi/lib/verify.d.ts +17 -0
  284. 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