@aryacode/dsh-engineering 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/cordis.patch.yml +122 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Independent saved structural/source evidence for native IDE mutations."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from .project import Project
|
|
6
|
+
from .cfb import CompoundFile
|
|
7
|
+
from .tree import parse_document, task_assignments
|
|
8
|
+
from .tasks import read_tasks
|
|
9
|
+
from .program_checks import mask
|
|
10
|
+
from .workflow import source_manifest
|
|
11
|
+
|
|
12
|
+
# Exact native empty GB emitted by MotionWorks IEC 3 Pro / Ade 1.19.
|
|
13
|
+
# Independently observed in two LD and two FBD creations. Unknown versions,
|
|
14
|
+
# truncated bodies and populated bodies must not be inferred to be empty.
|
|
15
|
+
EMPTY_GRAPH_SHA256 = '09985918e42e9108dd10f5d67cbafa125bd4938821c6c6efe160e735b49492bb'
|
|
16
|
+
|
|
17
|
+
def body_is_blank(language, raw, text=None):
|
|
18
|
+
if language in ('ST', 'IL'):
|
|
19
|
+
return text is not None and not text.strip()
|
|
20
|
+
return language in ('LD', 'FBD') and len(raw) == 292 and hashlib.sha256(raw).hexdigest() == EMPTY_GRAPH_SHA256
|
|
21
|
+
|
|
22
|
+
def snapshot(root):
|
|
23
|
+
project=Project(root)
|
|
24
|
+
document=parse_document(CompoundFile(root/'src.st1').read_stream('PROJECT.TRE').decode('latin1'))
|
|
25
|
+
if document.warnings:raise ValueError('Project tree parse warnings')
|
|
26
|
+
registry={}
|
|
27
|
+
for line in project.list_pou_lines():
|
|
28
|
+
columns=line.split('\t')
|
|
29
|
+
kind=columns[0].upper().replace('FUNCTIONBLOCK','FUNCTION_BLOCK')
|
|
30
|
+
if kind not in ('PROGRAM','FUNCTION','FUNCTION_BLOCK') or len(columns)<3:raise ValueError('Unsupported POU registry row')
|
|
31
|
+
if columns[1].casefold() in registry:raise ValueError('Duplicate POU registry row')
|
|
32
|
+
registry[columns[1].casefold()]=(kind,columns[2])
|
|
33
|
+
pous=[]
|
|
34
|
+
for pou in project.pous():
|
|
35
|
+
if pou.name.casefold() not in registry:raise ValueError('Unregistered POU directory')
|
|
36
|
+
table=pou.declarations()
|
|
37
|
+
if table.warnings:raise ValueError('Declaration parse warnings')
|
|
38
|
+
body_stream=pou.body_stream()
|
|
39
|
+
if not body_stream:raise ValueError('Missing POU body')
|
|
40
|
+
body=(pou.st_body() if body_stream[1]=='ST' else pou.text_body()) if body_stream[1] in ('ST','IL') else None
|
|
41
|
+
raw=body.encode('utf-8') if body is not None else pou.source().read_stream(body_stream[0])
|
|
42
|
+
variables=[dict(name=v.name,type=v.type_name,section=v.section,group=v.group,
|
|
43
|
+
address=v.address,initial_value=v.initial_value,description=v.description) for v in table.variables]
|
|
44
|
+
kind,_=registry[pou.name.casefold()]
|
|
45
|
+
parents=[node for node,_ in document.walk_with_ancestors()
|
|
46
|
+
if node.name.casefold()==pou.name.casefold() and
|
|
47
|
+
node.path.split('\t')[0].replace('\\','/').casefold()=='poe/'+pou.name.casefold()]
|
|
48
|
+
if len(parents)!=1:raise ValueError('POU registry/tree identity mismatch')
|
|
49
|
+
return_type=document.lines[parents[0].line+3].strip() if kind=='FUNCTION' else ''
|
|
50
|
+
if kind=='FUNCTION' and not return_type:raise ValueError('Missing function return type in saved tree')
|
|
51
|
+
pous.append(dict(name=pou.name,type=kind,return_type=return_type,language=body_stream[1],
|
|
52
|
+
body_sha256=hashlib.sha256(raw).hexdigest(),body_blank=body_is_blank(body_stream[1],raw,body),
|
|
53
|
+
variables=variables,identifiers=sorted(set(re.findall(r'[A-Za-z_][A-Za-z_0-9]*',mask(body or '').upper()))),
|
|
54
|
+
reference_scan_complete=body is not None))
|
|
55
|
+
if len(pous)!=len(registry):raise ValueError('Missing registered POU source')
|
|
56
|
+
task_nodes=[]
|
|
57
|
+
for node,_ in document.walk_with_ancestors():
|
|
58
|
+
path=node.path.split('\t')[0].replace('\\','/').split('/')
|
|
59
|
+
if len(path)==5 and path[0]=='C' and path[2]=='R' and path[-1]==node.name:
|
|
60
|
+
fields=node.path.split('\t')
|
|
61
|
+
instances=[]
|
|
62
|
+
for child in node.children:
|
|
63
|
+
program_type=document.lines[child.line+2].split('\t')[0]
|
|
64
|
+
if not program_type:raise ValueError('Missing saved program type')
|
|
65
|
+
instances.append(dict(name=child.name,type=program_type))
|
|
66
|
+
task_nodes.append(dict(name=node.name,kind=fields[3],instances=instances))
|
|
67
|
+
settings={t.name.casefold():t for t in read_tasks(root)}
|
|
68
|
+
if len(settings)!=len(task_nodes):raise ValueError('Task settings/tree inventory mismatch')
|
|
69
|
+
for task in task_nodes:
|
|
70
|
+
setting=settings[task['name'].casefold()]
|
|
71
|
+
if setting.warnings or setting.type!=task['kind']:raise ValueError('Task settings/tree disagree')
|
|
72
|
+
task['settings']=setting.fields
|
|
73
|
+
globals=project.global_variables()
|
|
74
|
+
if globals.warnings:raise ValueError('Global declaration parse warnings')
|
|
75
|
+
global_rows=[dict(name=v.name,type=v.type_name,section=v.section,group=v.group,
|
|
76
|
+
address=v.address,initial_value=v.initial_value,description=v.description) for v in globals.variables]
|
|
77
|
+
manifest=source_manifest(root)
|
|
78
|
+
program_sources={file:{name:sha for name,sha in streams.items() if name.upper().endswith(('.VB','.VGR','.STB','.AB','.GB','.TXT'))}
|
|
79
|
+
for file,streams in manifest['streams'].items()}
|
|
80
|
+
file_hashes=dict(manifest['files'])
|
|
81
|
+
for setting in settings.values():
|
|
82
|
+
file_hashes[setting.source.relative_to(root).as_posix()]=hashlib.sha256(setting.source.read_bytes()).hexdigest()
|
|
83
|
+
translations={}
|
|
84
|
+
empty_translations=[]
|
|
85
|
+
for pou in project.pous():
|
|
86
|
+
for path in pou.directory.iterdir():
|
|
87
|
+
if path.name.casefold().endswith('translation.xml'):
|
|
88
|
+
key=path.relative_to(root).as_posix()
|
|
89
|
+
translations[key]=hashlib.sha256(path.read_bytes()).hexdigest()
|
|
90
|
+
file_hashes[key]=translations[key]
|
|
91
|
+
try:
|
|
92
|
+
translation_document=ET.parse(path)
|
|
93
|
+
if translation_document.getroot().tag=='TranslationDocument' and not translation_document.findall('./ItemList/item'):
|
|
94
|
+
empty_translations.append(key)
|
|
95
|
+
except ET.ParseError:
|
|
96
|
+
pass # Older unused translation placeholders may be all zero bytes.
|
|
97
|
+
return dict(pous=pous,tasks=task_nodes,globals=global_rows,program_sources=program_sources,
|
|
98
|
+
translation_files=translations,empty_translation_files=empty_translations,file_hashes=file_hashes)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Exact saved PROGRAM instance ownership, separate from runtime execution."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .cfb import CompoundFile
|
|
6
|
+
from .tree import parse_document
|
|
7
|
+
from .tasks import parse_task_settings
|
|
8
|
+
|
|
9
|
+
IDENT = re.compile(r'^[A-Za-z_][A-Za-z_0-9]*$')
|
|
10
|
+
|
|
11
|
+
def resolve(document, programs):
|
|
12
|
+
if document.warnings:
|
|
13
|
+
raise ValueError('Task tree parse warnings')
|
|
14
|
+
known = {name.casefold(): name for name in programs}
|
|
15
|
+
if len(known) != len(programs): raise ValueError('Duplicate PROGRAM registry identity')
|
|
16
|
+
tasks, identities = [], set()
|
|
17
|
+
for node, _ in document.walk_with_ancestors():
|
|
18
|
+
fields = node.path.split('\t')
|
|
19
|
+
path = fields[0].replace('\\', '/').split('/')
|
|
20
|
+
if len(path) != 5 or path[0] != 'C' or path[2] != 'R' or path[-1] != node.name:
|
|
21
|
+
continue
|
|
22
|
+
if len(fields) < 4 or fields[3] not in ('CYCLIC','DEFAULT','SYSTEM') or not all(IDENT.fullmatch(p) for p in (path[1],path[3],node.name)):
|
|
23
|
+
raise ValueError('Unsupported task identity/kind')
|
|
24
|
+
identity = '/'.join(path)
|
|
25
|
+
if identity.casefold() in identities: raise ValueError('Duplicate task identity')
|
|
26
|
+
identities.add(identity.casefold()); instances, names = [], set()
|
|
27
|
+
for order, child in enumerate(node.children):
|
|
28
|
+
if child.children or not IDENT.fullmatch(child.name) or child.name.casefold() in names:
|
|
29
|
+
raise ValueError('Ambiguous PROGRAM instance identity')
|
|
30
|
+
names.add(child.name.casefold())
|
|
31
|
+
program = document.lines[child.line+2].split('\t')[0]
|
|
32
|
+
if program.casefold() not in known:
|
|
33
|
+
raise ValueError('Task instance type is absent from PROGRAM registry: '+program)
|
|
34
|
+
instances.append(dict(name=child.name,type=known[program.casefold()],order=order+1))
|
|
35
|
+
tasks.append(dict(name=node.name,configuration=path[1],resource=path[3],
|
|
36
|
+
path=identity,kind=fields[3],instances=instances))
|
|
37
|
+
return tasks
|
|
38
|
+
|
|
39
|
+
def inspect(root):
|
|
40
|
+
root = Path(root)
|
|
41
|
+
registry = root/'LIST.POU'; tree = root/'src.st1'
|
|
42
|
+
before = {str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in (registry,tree)}
|
|
43
|
+
programs, seen = [], set()
|
|
44
|
+
for line in registry.read_text(encoding='latin1').splitlines():
|
|
45
|
+
if not line.strip(): continue
|
|
46
|
+
columns = line.split('\t')
|
|
47
|
+
if len(columns)<2 or columns[0].upper().replace('FUNCTIONBLOCK','FUNCTION_BLOCK') not in ('PROGRAM','FUNCTION','FUNCTION_BLOCK') or not IDENT.fullmatch(columns[1]) or columns[1].casefold() in seen:
|
|
48
|
+
raise ValueError('Invalid/duplicate POU registry identity')
|
|
49
|
+
seen.add(columns[1].casefold())
|
|
50
|
+
if columns[0].upper() == 'PROGRAM': programs.append(columns[1])
|
|
51
|
+
document = parse_document(CompoundFile(tree).read_stream('PROJECT.TRE').decode('latin1'))
|
|
52
|
+
tasks = resolve(document,programs)
|
|
53
|
+
for task in tasks:
|
|
54
|
+
source = root/Path(task['path']).parent/(task['name']+'.SET')
|
|
55
|
+
if not source.resolve().is_relative_to(root.resolve()): raise ValueError('Task settings escape project')
|
|
56
|
+
raw = source.read_bytes(); before[str(source)] = hashlib.sha256(raw).hexdigest()
|
|
57
|
+
settings = parse_task_settings(raw.decode('latin1'),source)
|
|
58
|
+
if settings.warnings or settings.name.casefold()!=task['name'].casefold() or settings.type!=task['kind']:
|
|
59
|
+
raise ValueError('Task settings/tree identity or kind mismatch')
|
|
60
|
+
task['settings'] = settings.fields
|
|
61
|
+
task['settings_source'] = str(source)
|
|
62
|
+
for path, digest in before.items():
|
|
63
|
+
if hashlib.sha256(Path(path).read_bytes()).hexdigest()!=digest:
|
|
64
|
+
raise ValueError('Task sources changed during read')
|
|
65
|
+
assigned = {instance['type'].casefold() for task in tasks for instance in task['instances']}
|
|
66
|
+
return dict(bindings=tasks,program_types=programs,
|
|
67
|
+
unassigned=sorted(name for name in programs if name.casefold() not in assigned),
|
|
68
|
+
binding_resolution='exact_saved_tree',source_hashes=before)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Read MotionWorks task configuration.
|
|
2
|
+
|
|
3
|
+
Each resource task has a plain-text ``<Task>.SET`` file beside the resource
|
|
4
|
+
``src.st1``::
|
|
5
|
+
|
|
6
|
+
TASK FastTsk
|
|
7
|
+
(TYPE := CYCLIC,
|
|
8
|
+
INTERVAL := T#4ms,
|
|
9
|
+
PRIORITY := 0,
|
|
10
|
+
WATCHDOG := 4
|
|
11
|
+
WATCHDOG_DISPLAY := 4
|
|
12
|
+
WATCHDOG_ENABLED := YES
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
This is useful context for anyone editing motion code. Cycle time and priority
|
|
16
|
+
decide whether a block can finish in its task, and the watchdog decides what
|
|
17
|
+
happens when it cannot -- both matter more in motion control than in ordinary PLC
|
|
18
|
+
logic, and neither is visible from the POU bodies alone.
|
|
19
|
+
|
|
20
|
+
The format is close to IEC structured text but is not valid ST: fields are
|
|
21
|
+
newline-separated inside the parentheses and only the last one carries a
|
|
22
|
+
semicolon, so a real ST parser rejects it. This reader is therefore its own
|
|
23
|
+
small field parser rather than a reuse of the declaration parser.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from .errors import NotFound
|
|
33
|
+
|
|
34
|
+
#: ``TASK <name>`` on the first meaningful line.
|
|
35
|
+
_TASK_RE = re.compile(r"^\s*TASK\s+(\S+)", re.IGNORECASE)
|
|
36
|
+
#: ``FIELD := value`` with an optional trailing comma or semicolon.
|
|
37
|
+
_FIELD_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)[ \t]*:=[ \t]*([^,;\r\n]*)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TaskSettings:
|
|
42
|
+
"""One task's configuration."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
source: Path
|
|
46
|
+
fields: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
warnings: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def type(self) -> str | None:
|
|
51
|
+
return self.fields.get("TYPE")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def interval(self) -> str | None:
|
|
55
|
+
"""Cycle time such as ``T#4ms``, for cyclic tasks."""
|
|
56
|
+
return self.fields.get("INTERVAL")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def priority(self) -> int | None:
|
|
60
|
+
raw = self.fields.get("PRIORITY")
|
|
61
|
+
try:
|
|
62
|
+
return int(raw) if raw is not None else None
|
|
63
|
+
except ValueError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def watchdog(self) -> str | None:
|
|
68
|
+
return self.fields.get("WATCHDOG")
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def watchdog_enabled(self) -> bool | None:
|
|
72
|
+
raw = self.fields.get("WATCHDOG_ENABLED")
|
|
73
|
+
if raw is None:
|
|
74
|
+
return None
|
|
75
|
+
return raw.strip().upper() in {"YES", "TRUE", "1"}
|
|
76
|
+
|
|
77
|
+
def summary(self) -> str:
|
|
78
|
+
bits = [f"TYPE={self.type or '?'}"]
|
|
79
|
+
if self.interval:
|
|
80
|
+
bits.append(f"cycle={self.interval}")
|
|
81
|
+
if self.priority is not None:
|
|
82
|
+
bits.append(f"priority={self.priority}")
|
|
83
|
+
# Report the watchdog whenever we know anything about it. Gating this on
|
|
84
|
+
# a WATCHDOG value alone hid the "disabled" state for tasks that set only
|
|
85
|
+
# WATCHDOG_ENABLED, which is exactly the case worth surfacing.
|
|
86
|
+
if self.watchdog or self.watchdog_enabled is not None:
|
|
87
|
+
state = " (disabled)" if self.watchdog_enabled is False else ""
|
|
88
|
+
bits.append(f"watchdog={self.watchdog or 'n/a'}{state}")
|
|
89
|
+
for key in ("SPG", "INTERVAL_DISPLAY"):
|
|
90
|
+
if key in self.fields:
|
|
91
|
+
bits.append(f"{key.lower()}={self.fields[key]}")
|
|
92
|
+
return " ".join(bits)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_task_settings(text: str, source: Path | None = None) -> TaskSettings:
|
|
96
|
+
"""Parse a ``.SET`` payload.
|
|
97
|
+
|
|
98
|
+
Raises :class:`NotFound` when the text does not begin with a ``TASK`` line,
|
|
99
|
+
which is how a non-task ``.SET`` file is rejected rather than mis-parsed.
|
|
100
|
+
"""
|
|
101
|
+
match = _TASK_RE.search(text)
|
|
102
|
+
if match is None:
|
|
103
|
+
raise NotFound(
|
|
104
|
+
f"{source.name if source else '<text>'} is not a task settings file "
|
|
105
|
+
f"(no TASK line)"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
settings = TaskSettings(
|
|
109
|
+
name=match.group(1).strip(),
|
|
110
|
+
source=source or Path("<text>"),
|
|
111
|
+
)
|
|
112
|
+
for field_match in _FIELD_RE.finditer(text):
|
|
113
|
+
key = field_match.group(1).upper()
|
|
114
|
+
value = field_match.group(2).strip()
|
|
115
|
+
if key in settings.fields:
|
|
116
|
+
settings.warnings.append(f"duplicate field {key!r}; kept the first")
|
|
117
|
+
continue
|
|
118
|
+
settings.fields[key] = value
|
|
119
|
+
return settings
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def task_files(project_root: Path) -> list[Path]:
|
|
123
|
+
"""Every ``*.SET`` that looks like a task configuration in a project."""
|
|
124
|
+
root = Path(project_root)
|
|
125
|
+
found: list[Path] = []
|
|
126
|
+
for pattern in ("C/Configuration/R/Resource", "C/Resource/R/Resource"):
|
|
127
|
+
directory = root / pattern
|
|
128
|
+
if directory.is_dir():
|
|
129
|
+
found.extend(sorted(directory.glob("*.SET")))
|
|
130
|
+
return found
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_tasks(project_root: Path) -> list[TaskSettings]:
|
|
134
|
+
"""Read every task configuration in a project.
|
|
135
|
+
|
|
136
|
+
Task files sit beside the resource ``src.st1``. Files that are not task
|
|
137
|
+
settings are skipped rather than treated as errors, because the same
|
|
138
|
+
directory holds other ``.SET`` files.
|
|
139
|
+
"""
|
|
140
|
+
settings: list[TaskSettings] = []
|
|
141
|
+
for path in task_files(project_root):
|
|
142
|
+
try:
|
|
143
|
+
text = path.read_bytes().decode("latin1")
|
|
144
|
+
except OSError:
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
settings.append(parse_task_settings(text, path))
|
|
148
|
+
except NotFound:
|
|
149
|
+
continue
|
|
150
|
+
# Present cyclic tasks in ascending cycle time, which is how a user thinks
|
|
151
|
+
# about them: the fastest task is the one with the least time to spare.
|
|
152
|
+
def sort_key(item: TaskSettings):
|
|
153
|
+
interval = item.interval or ""
|
|
154
|
+
match = re.search(r"T#(\d+(?:\.\d+)?)(ms|us|s|ns)?", interval, re.IGNORECASE)
|
|
155
|
+
if not match:
|
|
156
|
+
return (2, 0.0, item.name)
|
|
157
|
+
value = float(match.group(1))
|
|
158
|
+
unit = (match.group(2) or "ms").lower()
|
|
159
|
+
scale = {"ns": 1e-6, "us": 1e-3, "ms": 1.0, "s": 1000.0}[unit]
|
|
160
|
+
return (0, value * scale, item.name)
|
|
161
|
+
|
|
162
|
+
return sorted(settings, key=sort_key)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Recoverable offline transactions. A successful file write is not a build verdict."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from .errors import MotionWorksError
|
|
14
|
+
from .ide import ensure_ide_closed
|
|
15
|
+
from .snapshot import sha256_file
|
|
16
|
+
from .staging import assert_proven, workspace_root
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def hash_tree(root):
|
|
20
|
+
"""A rollback snapshot includes generated files and hidden directories too."""
|
|
21
|
+
return {p.relative_to(root).as_posix(): sha256_file(p)
|
|
22
|
+
for p in sorted(Path(root).rglob('*')) if p.is_file()}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _save(path: Path, payload: dict) -> None:
|
|
26
|
+
temp = path.with_suffix('.tmp')
|
|
27
|
+
temp.write_text(json.dumps(payload, indent=2), encoding='utf-8')
|
|
28
|
+
os.replace(temp, path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _restore(root: Path, backup: Path, before: dict[str, str]) -> None:
|
|
32
|
+
# Verify the complete rollback set before replacing even one source file.
|
|
33
|
+
for name, digest in before.items():
|
|
34
|
+
if sha256_file(backup / name) != digest:
|
|
35
|
+
raise MotionWorksError(f'Rollback backup failed verification: {name}')
|
|
36
|
+
for path in sorted(root.rglob('*'), reverse=True):
|
|
37
|
+
if path.is_file() and path.relative_to(root).as_posix() not in before:
|
|
38
|
+
path.unlink()
|
|
39
|
+
for name in before:
|
|
40
|
+
target = root / name
|
|
41
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
temp = target.with_name(target.name + '.rollback-' + uuid4().hex)
|
|
43
|
+
shutil.copy2(backup / name, temp)
|
|
44
|
+
os.replace(temp, target)
|
|
45
|
+
for path in sorted(root.rglob('*'), reverse=True):
|
|
46
|
+
if path.is_dir() and not any(path.iterdir()):
|
|
47
|
+
path.rmdir()
|
|
48
|
+
if hash_tree(root) != before:
|
|
49
|
+
raise MotionWorksError('Rollback read-back differs from the recorded snapshot')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def update_metadata(root: Path) -> None:
|
|
53
|
+
path = root / 'PROJECT.INF'
|
|
54
|
+
if not path.exists():
|
|
55
|
+
return
|
|
56
|
+
raw = path.read_bytes()
|
|
57
|
+
if raw.startswith((b'\xff\xfe', b'\xfe\xff')):
|
|
58
|
+
encoding = 'utf-16'
|
|
59
|
+
else:
|
|
60
|
+
encoding = 'latin1'
|
|
61
|
+
text = raw.decode(encoding)
|
|
62
|
+
updated, count = re.subn(r'(?m)^LastChange=[^\r\n]*',
|
|
63
|
+
'LastChange=' + datetime.now().strftime('%m/%d/%Y %I:%M:%S %p'), text)
|
|
64
|
+
if count != 1:
|
|
65
|
+
raise MotionWorksError('PROJECT.INF must have exactly one LastChange field')
|
|
66
|
+
temp = path.with_name(path.name + '.transaction-' + uuid4().hex)
|
|
67
|
+
temp.write_bytes(updated.encode(encoding))
|
|
68
|
+
os.replace(temp, path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(project: Path, operation, *, validate=None) -> dict:
|
|
72
|
+
"""Commit a whole operation or restore its complete pre-operation file set.
|
|
73
|
+
|
|
74
|
+
The lock remains after a process crash or failed rollback. A later writer refuses;
|
|
75
|
+
it never silently treats an incomplete transaction as committed.
|
|
76
|
+
"""
|
|
77
|
+
root = assert_proven(project)
|
|
78
|
+
if root.suffix.lower() == '.mwt':
|
|
79
|
+
root = root.with_suffix('')
|
|
80
|
+
if not root.is_dir():
|
|
81
|
+
raise MotionWorksError('Transaction requires an expanded project directory')
|
|
82
|
+
ensure_ide_closed(auto_close=False)
|
|
83
|
+
home = workspace_root() / '.motionworks'
|
|
84
|
+
key = hashlib.sha256(str(root).casefold().encode()).hexdigest()[:20]
|
|
85
|
+
lock = home / 'locks' / (key + '.lock')
|
|
86
|
+
if not lock.resolve().is_relative_to(workspace_root()):
|
|
87
|
+
raise MotionWorksError('Transaction lock escapes the workspace')
|
|
88
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
transaction_id = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ-') + uuid4().hex[:8]
|
|
90
|
+
folder = home / 'transactions' / key / transaction_id
|
|
91
|
+
if not folder.resolve().is_relative_to(workspace_root()):
|
|
92
|
+
raise MotionWorksError('Transaction journal escapes the workspace')
|
|
93
|
+
try:
|
|
94
|
+
fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
95
|
+
except FileExistsError as exc:
|
|
96
|
+
raise MotionWorksError(f'Project transaction is locked; inspect {lock} before recovery') from exc
|
|
97
|
+
with os.fdopen(fd, 'w') as handle:
|
|
98
|
+
json.dump({'pid': os.getpid(), 'journal': str(folder / 'journal.json')}, handle)
|
|
99
|
+
clean = False
|
|
100
|
+
before = None
|
|
101
|
+
journal = {'id': transaction_id, 'project': str(root), 'state': 'preparing',
|
|
102
|
+
'verification': 'offline_only', 'required': ['rebuild', 'make', 'save', 'close_reopen']}
|
|
103
|
+
try:
|
|
104
|
+
folder.mkdir(parents=True)
|
|
105
|
+
before = hash_tree(root)
|
|
106
|
+
shutil.copytree(root, folder / 'before')
|
|
107
|
+
if hash_tree(folder / 'before') != before or hash_tree(root) != before:
|
|
108
|
+
raise MotionWorksError('Source changed during snapshot or backup did not verify')
|
|
109
|
+
journal.update(state='prepared', before=before)
|
|
110
|
+
_save(folder / 'journal.json', journal)
|
|
111
|
+
# Recheck after potentially slow backup, immediately before changing files.
|
|
112
|
+
ensure_ide_closed(auto_close=False)
|
|
113
|
+
journal['state'] = 'applying'
|
|
114
|
+
_save(folder / 'journal.json', journal)
|
|
115
|
+
payload = operation()
|
|
116
|
+
if payload.get('ok') is False:
|
|
117
|
+
raise MotionWorksError('Operation reported failure: ' + json.dumps(payload))
|
|
118
|
+
if validate:
|
|
119
|
+
validation = validate(root)
|
|
120
|
+
else:
|
|
121
|
+
from .validation import validate as validate_native
|
|
122
|
+
current = hash_tree(root)
|
|
123
|
+
changed_containers = [root / name for name, digest in current.items()
|
|
124
|
+
if Path(name).name.lower() == 'src.st1' and before.get(name) != digest]
|
|
125
|
+
validation = validate_native(root, containers=changed_containers) if changed_containers else {'status': 'operation_readback_only'}
|
|
126
|
+
if validation.get('ok') is False:
|
|
127
|
+
raise MotionWorksError('Static validation failed: ' + json.dumps(validation))
|
|
128
|
+
after = hash_tree(root)
|
|
129
|
+
if after != before:
|
|
130
|
+
update_metadata(root)
|
|
131
|
+
after = hash_tree(root)
|
|
132
|
+
changed = sorted(name for name in set(before) | set(after) if before.get(name) != after.get(name))
|
|
133
|
+
journal.update(state='committed', after=after, changed_files=changed, validation=validation)
|
|
134
|
+
_save(folder / 'journal.json', journal)
|
|
135
|
+
report = {k: journal[k] for k in ('id', 'state', 'verification', 'required', 'changed_files', 'validation')}
|
|
136
|
+
report.update(journal=str(folder / 'journal.json'), backup=str(folder / 'before'),
|
|
137
|
+
hashes={name: {'before': before.get(name), 'after': after.get(name)} for name in changed})
|
|
138
|
+
payload.setdefault('result', {})['transaction'] = report
|
|
139
|
+
clean = True
|
|
140
|
+
return payload
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
if journal['state'] in ('applying', 'committed'):
|
|
143
|
+
try:
|
|
144
|
+
ensure_ide_closed(auto_close=False)
|
|
145
|
+
assert_proven(root)
|
|
146
|
+
_restore(root, folder / 'before', before)
|
|
147
|
+
journal.update(state='rolled_back', error=str(exc))
|
|
148
|
+
_save(folder / 'journal.json', journal)
|
|
149
|
+
clean = True
|
|
150
|
+
except Exception as rollback_error:
|
|
151
|
+
raise MotionWorksError(f'Operation failed: {exc}; rollback needs recovery: {rollback_error}; journal: {folder}') from exc
|
|
152
|
+
else:
|
|
153
|
+
clean = True # No project mutation has begun.
|
|
154
|
+
raise MotionWorksError(f'Offline transaction failed: {exc}; journal: {folder}') from exc
|
|
155
|
+
finally:
|
|
156
|
+
if clean:
|
|
157
|
+
lock.unlink(missing_ok=True)
|