onec-interactive-runtime-core 0.1.19__py3-none-any.whl
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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
onec_runtime/capture.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from onec_runtime.bsl.lexer import KEYWORDS
|
|
6
|
+
from onec_runtime.experiment import bsl_string_literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _validate_capture_variable(name: str) -> None:
|
|
10
|
+
valid = bool(name) and (name[0].isalpha() or name[0] == "_")
|
|
11
|
+
valid = valid and all(character.isalnum() or character == "_" for character in name)
|
|
12
|
+
if not valid or name.upper() in KEYWORDS:
|
|
13
|
+
raise ValueError(f"Unsafe capture variable name: {name!r}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_capture_structure_expression(variable_names: Iterable[str]) -> str:
|
|
17
|
+
names: list[str] = []
|
|
18
|
+
seen: set[str] = set()
|
|
19
|
+
for name in variable_names:
|
|
20
|
+
_validate_capture_variable(name)
|
|
21
|
+
folded = name.casefold()
|
|
22
|
+
if folded in seen:
|
|
23
|
+
continue
|
|
24
|
+
seen.add(folded)
|
|
25
|
+
names.append(name)
|
|
26
|
+
if not names:
|
|
27
|
+
raise ValueError("At least one capture variable is required")
|
|
28
|
+
fields = ",".join(names)
|
|
29
|
+
values = ", ".join(names)
|
|
30
|
+
return f'Новый Структура("{fields}", {values})'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_extension_call(variable_names: Iterable[str], instruction: str) -> str:
|
|
34
|
+
structure = build_capture_structure_expression(variable_names)
|
|
35
|
+
code = bsl_string_literal(instruction)
|
|
36
|
+
return (
|
|
37
|
+
"RuntimeKernelServer.ВыполнитьВКонтекстеОтладки("
|
|
38
|
+
f"{structure}, {code})"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_lowered_extension_call(
|
|
43
|
+
variable_names: Iterable[str], instruction: str
|
|
44
|
+
) -> str:
|
|
45
|
+
structure = build_capture_structure_expression(variable_names)
|
|
46
|
+
code = bsl_string_literal(instruction)
|
|
47
|
+
return (
|
|
48
|
+
"RuntimeKernelServer.ВыполнитьПониженныйКодВКонтекстеОтладки("
|
|
49
|
+
f"{structure}, {code})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_capture_begin_call(variable_names: Iterable[str]) -> str:
|
|
54
|
+
structure = build_capture_structure_expression(variable_names)
|
|
55
|
+
return f"RuntimeKernelServer.НачатьКонтекстОтладки({structure})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_current_capture_call(instruction: str) -> str:
|
|
59
|
+
code = bsl_string_literal(instruction)
|
|
60
|
+
return (
|
|
61
|
+
"RuntimeKernelServer.ВыполнитьКодТекущегоКонтекстаОтладки("
|
|
62
|
+
f"{code})"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_capture_root_expression(name: str) -> str:
|
|
67
|
+
_validate_capture_variable(name)
|
|
68
|
+
return (
|
|
69
|
+
"RuntimeKernelServer.ПолучитьЗначениеКонтекстаОтладки("
|
|
70
|
+
f'"{name}")'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_capture_end_call() -> str:
|
|
75
|
+
return "RuntimeKernelServer.ЗавершитьКонтекстОтладки()"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_capture_transfer_call(variable_names: Iterable[str]) -> str:
|
|
79
|
+
structure = build_capture_structure_expression(variable_names)
|
|
80
|
+
return f"ПоместитьВоВременноеХранилище({structure})"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_live_capture_begin_call(address: str) -> str:
|
|
84
|
+
return (
|
|
85
|
+
"RuntimeKernelServer.НачатьКонтекстОтладкиВКонтексте(Контекст, "
|
|
86
|
+
+ bsl_string_literal(address)
|
|
87
|
+
+ ")"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_live_current_capture_call(instruction: str) -> str:
|
|
92
|
+
return (
|
|
93
|
+
"RuntimeKernelServer.ВыполнитьКодВКонтекстеОтладки(Контекст, "
|
|
94
|
+
+ bsl_string_literal(instruction)
|
|
95
|
+
+ ")"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_live_capture_root_transfer_call(name: str) -> str:
|
|
100
|
+
_validate_capture_variable(name)
|
|
101
|
+
return (
|
|
102
|
+
"RuntimeKernelServer.ПоместитьЗначениеКонтекстаОтладки(Контекст, "
|
|
103
|
+
+ bsl_string_literal(name)
|
|
104
|
+
+ ")"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_temporary_storage_value_expression(address: str) -> str:
|
|
109
|
+
return "ПолучитьИзВременногоХранилища(" + bsl_string_literal(address) + ")"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_live_capture_end_call() -> str:
|
|
113
|
+
return "RuntimeKernelServer.ЗавершитьКонтекстОтладкиВКонтексте(Контекст)"
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Target-neutral symbolic capture resolution for exported 1C source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from hashlib import sha256
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
from xml.etree import ElementTree
|
|
13
|
+
|
|
14
|
+
from onec_runtime.errors import ProtocolError
|
|
15
|
+
from onec_runtime.kernel import COMMON_MODULE_PROPERTY_ID
|
|
16
|
+
from onec_runtime.rdbg.models import ModuleLocation
|
|
17
|
+
|
|
18
|
+
MAX_SOURCE_EXCERPT = 4_096
|
|
19
|
+
_DECLARATION = re.compile(
|
|
20
|
+
r"^\s*(Процедура|Функция)\s+([\w\u0400-\u04ff]+)\s*\(",
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _identifier(value: object, *, name: str) -> str:
|
|
26
|
+
if not isinstance(value, str) or not value or len(value) > 256:
|
|
27
|
+
raise ValueError(f"{name} must be a bounded identifier")
|
|
28
|
+
if not value.isidentifier():
|
|
29
|
+
raise ValueError(f"{name} must be an identifier without expressions")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _qualified_identifier(value: object, *, name: str) -> str:
|
|
34
|
+
if not isinstance(value, str) or len(value) > 512:
|
|
35
|
+
raise ValueError(f"{name} must be bounded")
|
|
36
|
+
parts = value.split(".")
|
|
37
|
+
if not parts:
|
|
38
|
+
raise ValueError(f"{name} must be a qualified identifier")
|
|
39
|
+
for part in parts:
|
|
40
|
+
_identifier(part, name=name)
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _positive(value: object, *, name: str) -> int:
|
|
45
|
+
if type(value) is not int or value <= 0:
|
|
46
|
+
raise ValueError(f"{name} must be positive")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _sha256(value: object, *, name: str) -> str:
|
|
51
|
+
if (
|
|
52
|
+
not isinstance(value, str)
|
|
53
|
+
or len(value) != 64
|
|
54
|
+
or any(character not in "0123456789abcdef" for character in value)
|
|
55
|
+
):
|
|
56
|
+
raise ValueError(f"{name} must be a lowercase SHA-256 hex digest")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _mapping(value: object, *, name: str, allowed: set[str]) -> Mapping[str, object]:
|
|
61
|
+
if not isinstance(value, Mapping) or set(value) - allowed:
|
|
62
|
+
raise ValueError(f"{name} has unsupported fields")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class CaptureSourceConfig:
|
|
68
|
+
project: str
|
|
69
|
+
source_root: Path | str
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
object.__setattr__(self, "source_root", Path(self.source_root).resolve())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class CapturePointRequest:
|
|
77
|
+
name: str
|
|
78
|
+
project: str = ""
|
|
79
|
+
module: str = ""
|
|
80
|
+
procedure: str = ""
|
|
81
|
+
line: int | None = None
|
|
82
|
+
source_fragment: str | None = None
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
_identifier(self.name, name="capture point name")
|
|
86
|
+
has_location = any(
|
|
87
|
+
value not in {"", None}
|
|
88
|
+
for value in (self.project, self.module, self.procedure, self.line)
|
|
89
|
+
)
|
|
90
|
+
if has_location:
|
|
91
|
+
_identifier(self.project, name="project")
|
|
92
|
+
_qualified_identifier(self.module, name="module")
|
|
93
|
+
_identifier(self.procedure, name="procedure")
|
|
94
|
+
_positive(self.line, name="line")
|
|
95
|
+
if self.source_fragment is not None and (
|
|
96
|
+
not isinstance(self.source_fragment, str)
|
|
97
|
+
or not self.source_fragment.strip()
|
|
98
|
+
or len(self.source_fragment) > MAX_SOURCE_EXCERPT
|
|
99
|
+
):
|
|
100
|
+
raise ValueError("source_fragment must be a bounded non-empty string")
|
|
101
|
+
if not has_location and self.source_fragment is None:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"capture point requires a source location or stable fragment"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_wire(cls, value: object) -> CapturePointRequest:
|
|
108
|
+
raw = _mapping(
|
|
109
|
+
value,
|
|
110
|
+
name="capture point request",
|
|
111
|
+
allowed={
|
|
112
|
+
"name",
|
|
113
|
+
"project",
|
|
114
|
+
"module",
|
|
115
|
+
"procedure",
|
|
116
|
+
"line",
|
|
117
|
+
"source_fragment",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return cls(**dict(raw)) # type: ignore[arg-type]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class ResolvedCapturePoint:
|
|
125
|
+
name: str
|
|
126
|
+
project: str
|
|
127
|
+
module: str
|
|
128
|
+
procedure: str
|
|
129
|
+
line: int
|
|
130
|
+
source_revision: int
|
|
131
|
+
source_sha256: str
|
|
132
|
+
executable_line: int
|
|
133
|
+
excerpt: str
|
|
134
|
+
|
|
135
|
+
def __post_init__(self) -> None:
|
|
136
|
+
CapturePointRequest(
|
|
137
|
+
name=self.name,
|
|
138
|
+
project=self.project,
|
|
139
|
+
module=self.module,
|
|
140
|
+
procedure=self.procedure,
|
|
141
|
+
line=self.line,
|
|
142
|
+
)
|
|
143
|
+
_positive(self.source_revision, name="source_revision")
|
|
144
|
+
_sha256(self.source_sha256, name="source_sha256")
|
|
145
|
+
_positive(self.executable_line, name="executable_line")
|
|
146
|
+
if (
|
|
147
|
+
not isinstance(self.excerpt, str)
|
|
148
|
+
or not self.excerpt.strip()
|
|
149
|
+
or len(self.excerpt) > MAX_SOURCE_EXCERPT
|
|
150
|
+
):
|
|
151
|
+
raise ValueError("excerpt must be a bounded non-empty string")
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_wire(cls, value: object) -> ResolvedCapturePoint:
|
|
155
|
+
raw = _mapping(
|
|
156
|
+
value,
|
|
157
|
+
name="resolved capture point",
|
|
158
|
+
allowed={
|
|
159
|
+
"name",
|
|
160
|
+
"project",
|
|
161
|
+
"module",
|
|
162
|
+
"procedure",
|
|
163
|
+
"line",
|
|
164
|
+
"source_revision",
|
|
165
|
+
"source_sha256",
|
|
166
|
+
"executable_line",
|
|
167
|
+
"excerpt",
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
return cls(**dict(raw)) # type: ignore[arg-type]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True, slots=True)
|
|
174
|
+
class CaptureBinding:
|
|
175
|
+
point: ResolvedCapturePoint
|
|
176
|
+
location: ModuleLocation
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class CapturePointResolver(Protocol):
|
|
180
|
+
def resolve(
|
|
181
|
+
self, points: Sequence[CapturePointRequest]
|
|
182
|
+
) -> tuple[CaptureBinding, ...]: ...
|
|
183
|
+
|
|
184
|
+
def verify(self, bindings: Sequence[CaptureBinding]) -> None: ...
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True, slots=True)
|
|
188
|
+
class _ModuleSource:
|
|
189
|
+
name: str
|
|
190
|
+
path: Path
|
|
191
|
+
object_id: UUID
|
|
192
|
+
content: bytes
|
|
193
|
+
text: str
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def digest(self) -> str:
|
|
197
|
+
return sha256(self.content).hexdigest()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CommonModuleCaptureResolver:
|
|
201
|
+
"""Resolve stable source coordinates in an exported 1C source tree."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, project: str, source_root: Path) -> None:
|
|
204
|
+
if not isinstance(project, str) or not project.isidentifier():
|
|
205
|
+
raise ValueError("project must be an identifier")
|
|
206
|
+
self.project = project
|
|
207
|
+
self.source_root = Path(source_root).resolve()
|
|
208
|
+
self.extension_name = self._designer_extension_name()
|
|
209
|
+
|
|
210
|
+
def resolve(
|
|
211
|
+
self, points: Sequence[CapturePointRequest]
|
|
212
|
+
) -> tuple[CaptureBinding, ...]:
|
|
213
|
+
result: list[CaptureBinding] = []
|
|
214
|
+
for request in points:
|
|
215
|
+
if (
|
|
216
|
+
request.project
|
|
217
|
+
and request.project.casefold() != self.project.casefold()
|
|
218
|
+
):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
"capture point project does not match resolver project"
|
|
221
|
+
)
|
|
222
|
+
if request.module:
|
|
223
|
+
source = self._load_module(request.module)
|
|
224
|
+
line, procedure = self._resolve_in_module(source, request)
|
|
225
|
+
else:
|
|
226
|
+
source, line, procedure = self._resolve_fragment_globally(request)
|
|
227
|
+
excerpt = source.text.splitlines()[line - 1].strip()
|
|
228
|
+
resolved = ResolvedCapturePoint(
|
|
229
|
+
name=request.name,
|
|
230
|
+
project=self.project,
|
|
231
|
+
module=source.name,
|
|
232
|
+
procedure=procedure,
|
|
233
|
+
line=line,
|
|
234
|
+
source_revision=1,
|
|
235
|
+
source_sha256=source.digest,
|
|
236
|
+
executable_line=line,
|
|
237
|
+
excerpt=excerpt,
|
|
238
|
+
)
|
|
239
|
+
result.append(
|
|
240
|
+
CaptureBinding(
|
|
241
|
+
resolved,
|
|
242
|
+
ModuleLocation(
|
|
243
|
+
module_type=(
|
|
244
|
+
"ExtensionModule"
|
|
245
|
+
if self.extension_name
|
|
246
|
+
else "ConfigModule"
|
|
247
|
+
),
|
|
248
|
+
url="",
|
|
249
|
+
object_id=source.object_id,
|
|
250
|
+
property_id=UUID(COMMON_MODULE_PROPERTY_ID),
|
|
251
|
+
line=line,
|
|
252
|
+
extension_name=self.extension_name,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
return tuple(result)
|
|
257
|
+
|
|
258
|
+
def resolve_module_line(self, module_name: str, line: int) -> ModuleLocation:
|
|
259
|
+
"""Resolve a common-module source line to its debugger location."""
|
|
260
|
+
if type(line) is not int or line < 1:
|
|
261
|
+
raise ValueError("capture point line must be positive")
|
|
262
|
+
source = self._load_module(module_name)
|
|
263
|
+
lines = source.text.splitlines()
|
|
264
|
+
if line > len(lines):
|
|
265
|
+
raise ValueError("capture point line is outside module source")
|
|
266
|
+
excerpt = lines[line - 1].strip()
|
|
267
|
+
if (
|
|
268
|
+
not excerpt
|
|
269
|
+
or excerpt.startswith("//")
|
|
270
|
+
or _DECLARATION.match(excerpt)
|
|
271
|
+
or excerpt.casefold() in {"конецпроцедуры", "конецфункции"}
|
|
272
|
+
):
|
|
273
|
+
raise ValueError("capture point line is not executable source")
|
|
274
|
+
return ModuleLocation(
|
|
275
|
+
module_type="ExtensionModule" if self.extension_name else "ConfigModule",
|
|
276
|
+
url="",
|
|
277
|
+
object_id=source.object_id,
|
|
278
|
+
property_id=UUID(COMMON_MODULE_PROPERTY_ID),
|
|
279
|
+
line=line,
|
|
280
|
+
extension_name=self.extension_name,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def verify(self, bindings: Sequence[CaptureBinding]) -> None:
|
|
284
|
+
for binding in bindings:
|
|
285
|
+
source = self._load_module(binding.point.module)
|
|
286
|
+
if source.digest != binding.point.source_sha256:
|
|
287
|
+
raise ProtocolError("capture source changed after resolution")
|
|
288
|
+
|
|
289
|
+
def _resolve_in_module(
|
|
290
|
+
self,
|
|
291
|
+
source: _ModuleSource,
|
|
292
|
+
request: CapturePointRequest,
|
|
293
|
+
) -> tuple[int, str]:
|
|
294
|
+
regions = self._procedure_regions(source.text)
|
|
295
|
+
expected = request.procedure.casefold()
|
|
296
|
+
matching = [region for name, *region in regions if name.casefold() == expected]
|
|
297
|
+
if len(matching) != 1:
|
|
298
|
+
raise ValueError("capture point procedure was not found exactly once")
|
|
299
|
+
start, end = matching[0]
|
|
300
|
+
if request.source_fragment is not None:
|
|
301
|
+
matches = self._fragment_lines(
|
|
302
|
+
source.text, request.source_fragment, start, end
|
|
303
|
+
)
|
|
304
|
+
if len(matches) != 1:
|
|
305
|
+
raise ValueError("capture source fragment must occur exactly one time")
|
|
306
|
+
line = matches[0]
|
|
307
|
+
else:
|
|
308
|
+
assert request.line is not None
|
|
309
|
+
line = request.line
|
|
310
|
+
if not start <= line <= end:
|
|
311
|
+
raise ValueError("capture point line is outside the named procedure")
|
|
312
|
+
excerpt = source.text.splitlines()[line - 1].strip()
|
|
313
|
+
if not excerpt or excerpt.startswith("//"):
|
|
314
|
+
raise ValueError("capture point line is not executable source")
|
|
315
|
+
return line, request.procedure
|
|
316
|
+
|
|
317
|
+
def _resolve_fragment_globally(
|
|
318
|
+
self,
|
|
319
|
+
request: CapturePointRequest,
|
|
320
|
+
) -> tuple[_ModuleSource, int, str]:
|
|
321
|
+
assert request.source_fragment is not None
|
|
322
|
+
matches: list[tuple[_ModuleSource, int, str]] = []
|
|
323
|
+
for module_dir in self._common_modules().iterdir():
|
|
324
|
+
if not module_dir.is_dir():
|
|
325
|
+
continue
|
|
326
|
+
try:
|
|
327
|
+
source = self._load_module(module_dir.name)
|
|
328
|
+
except ProtocolError:
|
|
329
|
+
continue
|
|
330
|
+
for procedure, start, end in self._procedure_regions(source.text):
|
|
331
|
+
for line in self._fragment_lines(
|
|
332
|
+
source.text, request.source_fragment, start, end
|
|
333
|
+
):
|
|
334
|
+
matches.append((source, line, procedure))
|
|
335
|
+
if len(matches) != 1:
|
|
336
|
+
raise ValueError("capture source fragment must occur exactly one time")
|
|
337
|
+
return matches[0]
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def _fragment_lines(text: str, fragment: str, start: int, end: int) -> list[int]:
|
|
341
|
+
return [
|
|
342
|
+
number
|
|
343
|
+
for number, line in enumerate(text.splitlines(), start=1)
|
|
344
|
+
if start <= number <= end and fragment.strip() in line
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _procedure_regions(text: str) -> tuple[tuple[str, int, int], ...]:
|
|
349
|
+
lines = text.splitlines()
|
|
350
|
+
regions: list[tuple[str, int, int]] = []
|
|
351
|
+
active: tuple[str, int, str] | None = None
|
|
352
|
+
for number, line in enumerate(lines, start=1):
|
|
353
|
+
match = _DECLARATION.match(line)
|
|
354
|
+
if match is not None:
|
|
355
|
+
if active is not None:
|
|
356
|
+
raise ProtocolError("nested common-module procedure declaration")
|
|
357
|
+
kind = match.group(1).casefold()
|
|
358
|
+
terminator = "конецпроцедуры" if kind == "процедура" else "конецфункции"
|
|
359
|
+
active = (match.group(2), number, terminator)
|
|
360
|
+
continue
|
|
361
|
+
if active is not None and line.strip().casefold() == active[2]:
|
|
362
|
+
regions.append((active[0], active[1], number))
|
|
363
|
+
active = None
|
|
364
|
+
if active is not None:
|
|
365
|
+
raise ProtocolError("common-module procedure has no terminator")
|
|
366
|
+
return tuple(regions)
|
|
367
|
+
|
|
368
|
+
def _designer_extension_name(self) -> str:
|
|
369
|
+
configuration_path = self.source_root / "Configuration.xml"
|
|
370
|
+
if not configuration_path.is_file():
|
|
371
|
+
return ""
|
|
372
|
+
try:
|
|
373
|
+
root = ElementTree.parse(configuration_path).getroot()
|
|
374
|
+
configuration = next(
|
|
375
|
+
element
|
|
376
|
+
for element in root.iter()
|
|
377
|
+
if element.tag.rsplit("}", 1)[-1] == "Configuration"
|
|
378
|
+
)
|
|
379
|
+
properties = next(
|
|
380
|
+
element
|
|
381
|
+
for element in configuration
|
|
382
|
+
if element.tag.rsplit("}", 1)[-1] == "Properties"
|
|
383
|
+
)
|
|
384
|
+
values = {
|
|
385
|
+
element.tag.rsplit("}", 1)[-1]: element.text or ""
|
|
386
|
+
for element in properties
|
|
387
|
+
}
|
|
388
|
+
except (OSError, ElementTree.ParseError, StopIteration) as error:
|
|
389
|
+
raise ProtocolError(
|
|
390
|
+
f"configuration source is invalid: {configuration_path}"
|
|
391
|
+
) from error
|
|
392
|
+
if "ConfigurationExtensionPurpose" not in values:
|
|
393
|
+
return ""
|
|
394
|
+
name = values.get("Name", "")
|
|
395
|
+
try:
|
|
396
|
+
return _identifier(name, name="extension name")
|
|
397
|
+
except ValueError as error:
|
|
398
|
+
raise ProtocolError(
|
|
399
|
+
f"configuration extension name is invalid: {configuration_path}"
|
|
400
|
+
) from error
|
|
401
|
+
|
|
402
|
+
def _common_modules(self) -> Path:
|
|
403
|
+
path = self.source_root / "CommonModules"
|
|
404
|
+
if not path.is_dir():
|
|
405
|
+
raise ProtocolError("common-module source root is missing")
|
|
406
|
+
return path
|
|
407
|
+
|
|
408
|
+
def _load_module(self, module_name: str) -> _ModuleSource:
|
|
409
|
+
leaf_name = module_name.rsplit(".", 1)[-1]
|
|
410
|
+
module_root = self._common_modules() / leaf_name
|
|
411
|
+
edt_metadata = module_root / f"{leaf_name}.mdo"
|
|
412
|
+
edt_source = module_root / "Module.bsl"
|
|
413
|
+
designer_metadata = self._common_modules() / f"{leaf_name}.xml"
|
|
414
|
+
designer_source = module_root / "Ext" / "Module.bsl"
|
|
415
|
+
if edt_metadata.is_file() and edt_source.is_file():
|
|
416
|
+
metadata_path, source_path = edt_metadata, edt_source
|
|
417
|
+
elif designer_metadata.is_file() and designer_source.is_file():
|
|
418
|
+
metadata_path, source_path = designer_metadata, designer_source
|
|
419
|
+
else:
|
|
420
|
+
raise ProtocolError(f"common module source is missing: {module_name}")
|
|
421
|
+
try:
|
|
422
|
+
root = ElementTree.parse(metadata_path).getroot()
|
|
423
|
+
candidates = [
|
|
424
|
+
element
|
|
425
|
+
for element in root.iter()
|
|
426
|
+
if element.tag.rsplit("}", 1)[-1] == "CommonModule"
|
|
427
|
+
]
|
|
428
|
+
owner = root if "uuid" in root.attrib else candidates[0]
|
|
429
|
+
object_id = UUID(owner.attrib["uuid"])
|
|
430
|
+
content = source_path.read_bytes()
|
|
431
|
+
text = content.decode("utf-8-sig")
|
|
432
|
+
except (
|
|
433
|
+
OSError,
|
|
434
|
+
UnicodeDecodeError,
|
|
435
|
+
ElementTree.ParseError,
|
|
436
|
+
IndexError,
|
|
437
|
+
KeyError,
|
|
438
|
+
ValueError,
|
|
439
|
+
) as error:
|
|
440
|
+
raise ProtocolError(
|
|
441
|
+
f"common module source is invalid: {module_name}"
|
|
442
|
+
) from error
|
|
443
|
+
return _ModuleSource(module_name, source_path, object_id, content, text)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
__all__ = [
|
|
447
|
+
"CaptureBinding",
|
|
448
|
+
"CapturePointRequest",
|
|
449
|
+
"CapturePointResolver",
|
|
450
|
+
"CaptureSourceConfig",
|
|
451
|
+
"CommonModuleCaptureResolver",
|
|
452
|
+
"ResolvedCapturePoint",
|
|
453
|
+
]
|