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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from onec_runtime.bsl.source_maps import SourceSpan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
KEYWORDS = {
|
|
10
|
+
"ПЕРЕМ": "ПЕРЕМ", "VAR": "ПЕРЕМ",
|
|
11
|
+
"ЭКСПОРТ": "ЭКСПОРТ", "EXPORT": "ЭКСПОРТ",
|
|
12
|
+
"АСИНХ": "АСИНХ", "ASYNC": "АСИНХ",
|
|
13
|
+
"ПРОЦЕДУРА": "ПРОЦЕДУРА", "PROCEDURE": "ПРОЦЕДУРА",
|
|
14
|
+
"КОНЕЦПРОЦЕДУРЫ": "КОНЕЦПРОЦЕДУРЫ", "ENDPROCEDURE": "КОНЕЦПРОЦЕДУРЫ",
|
|
15
|
+
"ФУНКЦИЯ": "ФУНКЦИЯ", "FUNCTION": "ФУНКЦИЯ",
|
|
16
|
+
"КОНЕЦФУНКЦИИ": "КОНЕЦФУНКЦИИ", "ENDFUNCTION": "КОНЕЦФУНКЦИИ",
|
|
17
|
+
"ЗНАЧ": "ЗНАЧ", "VAL": "ЗНАЧ",
|
|
18
|
+
"ЕСЛИ": "ЕСЛИ", "IF": "ЕСЛИ", "ТОГДА": "ТОГДА", "THEN": "ТОГДА",
|
|
19
|
+
"ИНАЧЕЕСЛИ": "ИНАЧЕЕСЛИ", "ELSIF": "ИНАЧЕЕСЛИ",
|
|
20
|
+
"ИНАЧЕ": "ИНАЧЕ", "ELSE": "ИНАЧЕ", "КОНЕЦЕСЛИ": "КОНЕЦЕСЛИ", "ENDIF": "КОНЕЦЕСЛИ",
|
|
21
|
+
"ПОКА": "ПОКА", "WHILE": "ПОКА", "ЦИКЛ": "ЦИКЛ", "DO": "ЦИКЛ",
|
|
22
|
+
"КОНЕЦЦИКЛА": "КОНЕЦЦИКЛА", "ENDDO": "КОНЕЦЦИКЛА",
|
|
23
|
+
"ДЛЯ": "ДЛЯ", "FOR": "ДЛЯ", "КАЖДОГО": "КАЖДОГО", "EACH": "КАЖДОГО",
|
|
24
|
+
"ИЗ": "ИЗ", "IN": "ИЗ", "ПО": "ПО", "TO": "ПО",
|
|
25
|
+
"ПОПЫТКА": "ПОПЫТКА", "TRY": "ПОПЫТКА", "ИСКЛЮЧЕНИЕ": "ИСКЛЮЧЕНИЕ", "EXCEPT": "ИСКЛЮЧЕНИЕ",
|
|
26
|
+
"КОНЕЦПОПЫТКИ": "КОНЕЦПОПЫТКИ", "ENDTRY": "КОНЕЦПОПЫТКИ",
|
|
27
|
+
"ВОЗВРАТ": "ВОЗВРАТ", "RETURN": "ВОЗВРАТ", "ПРОДОЛЖИТЬ": "ПРОДОЛЖИТЬ", "CONTINUE": "ПРОДОЛЖИТЬ",
|
|
28
|
+
"ПРЕРВАТЬ": "ПРЕРВАТЬ", "BREAK": "ПРЕРВАТЬ", "ВЫЗВАТЬИСКЛЮЧЕНИЕ": "ВЫЗВАТЬИСКЛЮЧЕНИЕ", "RAISE": "ВЫЗВАТЬИСКЛЮЧЕНИЕ",
|
|
29
|
+
"ВЫПОЛНИТЬ": "ВЫПОЛНИТЬ", "EXECUTE": "ВЫПОЛНИТЬ", "ПЕРЕЙТИ": "ПЕРЕЙТИ", "GOTO": "ПЕРЕЙТИ",
|
|
30
|
+
"ДОБАВИТЬОБРАБОТЧИК": "ДОБАВИТЬОБРАБОТЧИК", "ADDHANDLER": "ДОБАВИТЬОБРАБОТЧИК",
|
|
31
|
+
"УДАЛИТЬОБРАБОТЧИК": "УДАЛИТЬОБРАБОТЧИК", "REMOVEHANDLER": "УДАЛИТЬОБРАБОТЧИК",
|
|
32
|
+
"ЖДАТЬ": "ЖДАТЬ", "AWAIT": "ЖДАТЬ", "И": "И", "AND": "И", "ИЛИ": "ИЛИ", "OR": "ИЛИ", "НЕ": "НЕ", "NOT": "НЕ",
|
|
33
|
+
"НОВЫЙ": "НОВЫЙ", "NEW": "НОВЫЙ", "ИСТИНА": "ИСТИНА", "TRUE": "ИСТИНА", "ЛОЖЬ": "ЛОЖЬ", "FALSE": "ЛОЖЬ",
|
|
34
|
+
"НЕОПРЕДЕЛЕНО": "НЕОПРЕДЕЛЕНО", "UNDEFINED": "НЕОПРЕДЕЛЕНО", "NULL": "NULL",
|
|
35
|
+
"ОБЛАСТЬ": "ОБЛАСТЬ", "REGION": "ОБЛАСТЬ",
|
|
36
|
+
"КОНЕЦОБЛАСТИ": "КОНЕЦОБЛАСТИ", "ENDREGION": "КОНЕЦОБЛАСТИ",
|
|
37
|
+
"ИСПОЛЬЗОВАТЬ": "ИСПОЛЬЗОВАТЬ", "USE": "ИСПОЛЬЗОВАТЬ",
|
|
38
|
+
"NATIVE": "NATIVE", "STACK": "STACK",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_PREPROCESSOR_ONLY_KEYWORDS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"ОБЛАСТЬ",
|
|
44
|
+
"REGION",
|
|
45
|
+
"КОНЕЦОБЛАСТИ",
|
|
46
|
+
"ENDREGION",
|
|
47
|
+
"ИСПОЛЬЗОВАТЬ",
|
|
48
|
+
"USE",
|
|
49
|
+
"NATIVE",
|
|
50
|
+
"STACK",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_ID = re.compile(r"[A-Za-zА-Яа-яЁё_][0-9A-Za-zА-Яа-яЁё_]*")
|
|
55
|
+
_NUMBER = re.compile(r"(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class Token:
|
|
60
|
+
type: str
|
|
61
|
+
text: str
|
|
62
|
+
start: int
|
|
63
|
+
end: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BslLexError(ValueError):
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
message: str,
|
|
70
|
+
*,
|
|
71
|
+
span: SourceSpan = SourceSpan(0, 0),
|
|
72
|
+
code: str = "lexical_error",
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
self.span = span
|
|
76
|
+
self.code = code
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def tokenize(source: str) -> list[Token]:
|
|
80
|
+
result: list[Token] = []
|
|
81
|
+
position = 0
|
|
82
|
+
force_identifier = False
|
|
83
|
+
while position < len(source):
|
|
84
|
+
if source[position].isspace():
|
|
85
|
+
position += 1
|
|
86
|
+
continue
|
|
87
|
+
if source.startswith("//", position):
|
|
88
|
+
newline = source.find("\n", position)
|
|
89
|
+
position = len(source) if newline < 0 else newline + 1
|
|
90
|
+
continue
|
|
91
|
+
start = position
|
|
92
|
+
character = source[position]
|
|
93
|
+
if character == '"':
|
|
94
|
+
position += 1
|
|
95
|
+
while position < len(source):
|
|
96
|
+
if source[position] == '"':
|
|
97
|
+
if position + 1 < len(source) and source[position + 1] == '"':
|
|
98
|
+
position += 2
|
|
99
|
+
continue
|
|
100
|
+
position += 1
|
|
101
|
+
break
|
|
102
|
+
position += 1
|
|
103
|
+
else:
|
|
104
|
+
raise BslLexError(
|
|
105
|
+
f"Unterminated string at {start}",
|
|
106
|
+
span=SourceSpan(start, len(source)),
|
|
107
|
+
code="unterminated_string",
|
|
108
|
+
)
|
|
109
|
+
result.append(Token("STRING", source[start:position], start, position))
|
|
110
|
+
force_identifier = False
|
|
111
|
+
continue
|
|
112
|
+
if character == "'":
|
|
113
|
+
position = source.find("'", position + 1)
|
|
114
|
+
if position < 0:
|
|
115
|
+
raise BslLexError(
|
|
116
|
+
f"Unterminated date literal at {start}",
|
|
117
|
+
span=SourceSpan(start, len(source)),
|
|
118
|
+
code="unterminated_date_literal",
|
|
119
|
+
)
|
|
120
|
+
position += 1
|
|
121
|
+
result.append(Token("DATETIME", source[start:position], start, position))
|
|
122
|
+
force_identifier = False
|
|
123
|
+
continue
|
|
124
|
+
match = _NUMBER.match(source, position)
|
|
125
|
+
if match:
|
|
126
|
+
position = match.end()
|
|
127
|
+
result.append(Token("NUMBER", match.group(), start, position))
|
|
128
|
+
force_identifier = False
|
|
129
|
+
continue
|
|
130
|
+
match = _ID.match(source, position)
|
|
131
|
+
if match:
|
|
132
|
+
position = match.end()
|
|
133
|
+
text = match.group()
|
|
134
|
+
keyword = text.upper()
|
|
135
|
+
preprocessor_only = keyword in _PREPROCESSOR_ONLY_KEYWORDS
|
|
136
|
+
follows_hash = bool(result and result[-1].type == "#")
|
|
137
|
+
token_type = (
|
|
138
|
+
"ID"
|
|
139
|
+
if force_identifier or (preprocessor_only and not follows_hash)
|
|
140
|
+
else KEYWORDS.get(keyword, "ID")
|
|
141
|
+
)
|
|
142
|
+
result.append(Token(token_type, text, start, position))
|
|
143
|
+
force_identifier = False
|
|
144
|
+
continue
|
|
145
|
+
operator = next(
|
|
146
|
+
(value for value in ("<=", ">=", "<>") if source.startswith(value, position)),
|
|
147
|
+
None,
|
|
148
|
+
)
|
|
149
|
+
if operator:
|
|
150
|
+
position += len(operator)
|
|
151
|
+
result.append(Token(operator, operator, start, position))
|
|
152
|
+
force_identifier = False
|
|
153
|
+
continue
|
|
154
|
+
if character in ";,().[]+-*/%=<>?:~#&":
|
|
155
|
+
position += 1
|
|
156
|
+
result.append(Token(character, character, start, position))
|
|
157
|
+
force_identifier = character in ".&"
|
|
158
|
+
continue
|
|
159
|
+
raise BslLexError(
|
|
160
|
+
f"Unexpected character {character!r} at {position}",
|
|
161
|
+
span=SourceSpan(position, position + 1),
|
|
162
|
+
code="unexpected_character",
|
|
163
|
+
)
|
|
164
|
+
return result
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Lazy Designer/EDT common-module metadata catalog for one runtime session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from hashlib import sha256
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
from threading import RLock
|
|
14
|
+
import xml.etree.ElementTree as ElementTree
|
|
15
|
+
|
|
16
|
+
from onec_runtime.errors import ModuleUniverseAdmissionError, ProtocolError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_BSL_IDENTIFIER_RE = re.compile(r"[A-Za-zА-Яа-яЁё_][0-9A-Za-zА-Яа-яЁё_]*\Z")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CommonModuleScope(str, Enum):
|
|
23
|
+
SERVER = "server"
|
|
24
|
+
CLIENT_SERVER = "client_server"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class CommonModuleDescriptor:
|
|
29
|
+
canonical_name: str
|
|
30
|
+
scope: CommonModuleScope
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class CommonModuleCatalogSnapshot:
|
|
35
|
+
profile: str
|
|
36
|
+
preprocessor_profile: str
|
|
37
|
+
revision: int
|
|
38
|
+
modules: tuple[CommonModuleDescriptor, ...]
|
|
39
|
+
sha256: str
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(
|
|
43
|
+
cls,
|
|
44
|
+
*,
|
|
45
|
+
profile: str,
|
|
46
|
+
preprocessor_profile: str,
|
|
47
|
+
revision: int,
|
|
48
|
+
modules: Iterable[CommonModuleDescriptor],
|
|
49
|
+
) -> "CommonModuleCatalogSnapshot":
|
|
50
|
+
ordered = tuple(sorted(modules, key=lambda item: item.canonical_name.casefold()))
|
|
51
|
+
payload = json.dumps(
|
|
52
|
+
{
|
|
53
|
+
"profile": profile,
|
|
54
|
+
"preprocessor_profile": preprocessor_profile,
|
|
55
|
+
"modules": [
|
|
56
|
+
[item.canonical_name.casefold(), item.scope.value]
|
|
57
|
+
for item in ordered
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
ensure_ascii=False,
|
|
61
|
+
sort_keys=True,
|
|
62
|
+
separators=(",", ":"),
|
|
63
|
+
).encode("utf-8")
|
|
64
|
+
return cls(
|
|
65
|
+
profile,
|
|
66
|
+
preprocessor_profile,
|
|
67
|
+
revision,
|
|
68
|
+
ordered,
|
|
69
|
+
sha256(payload).hexdigest(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def require(self, name: str) -> CommonModuleDescriptor:
|
|
73
|
+
by_name = {item.canonical_name.casefold(): item for item in self.modules}
|
|
74
|
+
try:
|
|
75
|
+
return by_name[name.casefold()]
|
|
76
|
+
except (AttributeError, KeyError):
|
|
77
|
+
raise ModuleUniverseAdmissionError(
|
|
78
|
+
"common module is absent from catalog"
|
|
79
|
+
) from None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SessionCommonModuleCatalog:
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
source_root: Path,
|
|
86
|
+
*,
|
|
87
|
+
profile: str,
|
|
88
|
+
preprocessor_profile: str = "server",
|
|
89
|
+
) -> None:
|
|
90
|
+
supplied = Path(source_root)
|
|
91
|
+
if _is_link(supplied):
|
|
92
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
93
|
+
try:
|
|
94
|
+
resolved = supplied.resolve(strict=True)
|
|
95
|
+
configured_root = resolved
|
|
96
|
+
direct = resolved / "CommonModules"
|
|
97
|
+
nested_root = resolved / "src"
|
|
98
|
+
nested = nested_root / "CommonModules"
|
|
99
|
+
if _is_link(direct) or _is_link(nested_root) or _is_link(nested):
|
|
100
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
101
|
+
if direct.is_dir() and nested.is_dir():
|
|
102
|
+
raise ProtocolError("common-module source root is ambiguous")
|
|
103
|
+
if not direct.is_dir() and nested.is_dir():
|
|
104
|
+
resolved = nested_root.resolve(strict=True)
|
|
105
|
+
supplied_common_modules = resolved / "CommonModules"
|
|
106
|
+
if _is_link(supplied_common_modules):
|
|
107
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
108
|
+
common_modules = supplied_common_modules.resolve(strict=True)
|
|
109
|
+
except (FileNotFoundError, OSError) as error:
|
|
110
|
+
raise ProtocolError("common-module source root is unsafe") from error
|
|
111
|
+
if (
|
|
112
|
+
not resolved.is_dir()
|
|
113
|
+
or _is_link(common_modules)
|
|
114
|
+
or not common_modules.is_dir()
|
|
115
|
+
or not common_modules.is_relative_to(resolved)
|
|
116
|
+
or common_modules.parent != resolved
|
|
117
|
+
):
|
|
118
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
119
|
+
self._source_root = resolved
|
|
120
|
+
self._configured_root = configured_root
|
|
121
|
+
self._common_modules = common_modules
|
|
122
|
+
self._profile = profile
|
|
123
|
+
self._preprocessor_profile = preprocessor_profile
|
|
124
|
+
self._snapshot: CommonModuleCatalogSnapshot | None = None
|
|
125
|
+
self._path_index: dict[str, Path] | None = None
|
|
126
|
+
self._resolved: dict[str, CommonModuleDescriptor | None] = {}
|
|
127
|
+
self._lock = RLock()
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def initialized(self) -> bool:
|
|
131
|
+
with self._lock:
|
|
132
|
+
return self._snapshot is not None
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def source_root(self) -> Path:
|
|
136
|
+
"""Normalized Designer root or EDT src directory."""
|
|
137
|
+
return self._source_root
|
|
138
|
+
|
|
139
|
+
def read_worker_module_source(self, path: Path | str) -> tuple[str, str]:
|
|
140
|
+
"""Read one canonical Designer or EDT module under the bound root.
|
|
141
|
+
|
|
142
|
+
A relative path may start at the configured EDT project root or its src.
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(path, (Path, str)):
|
|
145
|
+
raise ProtocolError("Worker module source path is invalid")
|
|
146
|
+
supplied = Path(path)
|
|
147
|
+
if supplied.is_absolute():
|
|
148
|
+
candidate = supplied
|
|
149
|
+
elif (
|
|
150
|
+
self._configured_root != self._source_root
|
|
151
|
+
and supplied.parts
|
|
152
|
+
and supplied.parts[0].casefold() == "src"
|
|
153
|
+
):
|
|
154
|
+
candidate = self._configured_root / supplied
|
|
155
|
+
else:
|
|
156
|
+
candidate = self._source_root / supplied
|
|
157
|
+
try:
|
|
158
|
+
relative = candidate.relative_to(self._source_root)
|
|
159
|
+
except ValueError:
|
|
160
|
+
raise ProtocolError("Worker module source path is outside source root") from None
|
|
161
|
+
parts = relative.parts
|
|
162
|
+
if (
|
|
163
|
+
len(parts) == 4
|
|
164
|
+
and parts[0].casefold() == "commonmodules"
|
|
165
|
+
and parts[2].casefold() == "ext"
|
|
166
|
+
and parts[3].casefold() == "module.bsl"
|
|
167
|
+
):
|
|
168
|
+
name = parts[1]
|
|
169
|
+
metadata = self._common_modules / f"{name}.xml"
|
|
170
|
+
elif (
|
|
171
|
+
len(parts) == 3
|
|
172
|
+
and parts[0].casefold() == "commonmodules"
|
|
173
|
+
and parts[2].casefold() == "module.bsl"
|
|
174
|
+
):
|
|
175
|
+
name = parts[1]
|
|
176
|
+
metadata = self._common_modules / name / f"{name}.mdo"
|
|
177
|
+
else:
|
|
178
|
+
raise ProtocolError("Worker module source path is invalid")
|
|
179
|
+
if not _is_bsl_identifier(name):
|
|
180
|
+
raise ProtocolError("Worker module source path is invalid")
|
|
181
|
+
component = self._source_root
|
|
182
|
+
for part in parts:
|
|
183
|
+
component = component / part
|
|
184
|
+
if _is_link(component):
|
|
185
|
+
raise ProtocolError("Worker module source path is unsafe")
|
|
186
|
+
try:
|
|
187
|
+
resolved = candidate.resolve(strict=True)
|
|
188
|
+
if (
|
|
189
|
+
resolved != candidate
|
|
190
|
+
or not resolved.is_file()
|
|
191
|
+
or not metadata.is_file()
|
|
192
|
+
or _is_link(metadata)
|
|
193
|
+
):
|
|
194
|
+
raise ProtocolError("Worker module source path is invalid")
|
|
195
|
+
return name, resolved.read_bytes().decode("utf-8-sig")
|
|
196
|
+
except (OSError, UnicodeError) as error:
|
|
197
|
+
raise ProtocolError("Worker module source path is unreadable") from error
|
|
198
|
+
|
|
199
|
+
def ensure_initialized(self) -> CommonModuleCatalogSnapshot:
|
|
200
|
+
with self._lock:
|
|
201
|
+
if self._snapshot is None:
|
|
202
|
+
self._path_index = self._enumerate_path_index()
|
|
203
|
+
self._snapshot = CommonModuleCatalogSnapshot.create(
|
|
204
|
+
profile=self._profile,
|
|
205
|
+
preprocessor_profile=self._preprocessor_profile,
|
|
206
|
+
revision=1,
|
|
207
|
+
modules=(),
|
|
208
|
+
)
|
|
209
|
+
return self._snapshot
|
|
210
|
+
|
|
211
|
+
def resolve_candidates(self, names: Iterable[str]) -> CommonModuleCatalogSnapshot:
|
|
212
|
+
"""Resolve permissive bare roots, ignoring missing/non-server modules."""
|
|
213
|
+
|
|
214
|
+
return self._resolve_names(names, required=False)
|
|
215
|
+
|
|
216
|
+
def ensure_modules(self, names: Iterable[str]) -> CommonModuleCatalogSnapshot:
|
|
217
|
+
"""Resolve required loaded-module identities, failing closed."""
|
|
218
|
+
|
|
219
|
+
return self._resolve_names(names, required=True)
|
|
220
|
+
|
|
221
|
+
def _resolve_names(
|
|
222
|
+
self,
|
|
223
|
+
names: Iterable[str],
|
|
224
|
+
*,
|
|
225
|
+
required: bool,
|
|
226
|
+
) -> CommonModuleCatalogSnapshot:
|
|
227
|
+
with self._lock:
|
|
228
|
+
current = self.ensure_initialized()
|
|
229
|
+
requested: dict[str, str] = {}
|
|
230
|
+
for name in names:
|
|
231
|
+
if not _is_bsl_identifier(name):
|
|
232
|
+
raise ProtocolError("common-module batch identity is invalid")
|
|
233
|
+
requested.setdefault(name.casefold(), name)
|
|
234
|
+
if not requested:
|
|
235
|
+
return current
|
|
236
|
+
|
|
237
|
+
staged_resolved = dict(self._resolved)
|
|
238
|
+
staged_index = self._path_index or {}
|
|
239
|
+
if any(
|
|
240
|
+
normalized not in staged_index and normalized not in staged_resolved
|
|
241
|
+
for normalized in requested
|
|
242
|
+
):
|
|
243
|
+
staged_index = self._enumerate_path_index()
|
|
244
|
+
staged_modules = {
|
|
245
|
+
item.canonical_name.casefold(): item for item in current.modules
|
|
246
|
+
}
|
|
247
|
+
changed = False
|
|
248
|
+
for normalized in requested:
|
|
249
|
+
if normalized in staged_resolved:
|
|
250
|
+
descriptor = staged_resolved[normalized]
|
|
251
|
+
else:
|
|
252
|
+
metadata_path = staged_index.get(normalized)
|
|
253
|
+
if metadata_path is None:
|
|
254
|
+
if required:
|
|
255
|
+
raise ProtocolError("missing common module is unsupported")
|
|
256
|
+
continue
|
|
257
|
+
descriptor = self._read_descriptor(metadata_path)
|
|
258
|
+
staged_resolved[normalized] = descriptor
|
|
259
|
+
|
|
260
|
+
if descriptor is None:
|
|
261
|
+
if required:
|
|
262
|
+
raise ProtocolError("missing common module is unsupported")
|
|
263
|
+
continue
|
|
264
|
+
if normalized not in staged_modules:
|
|
265
|
+
staged_modules[normalized] = descriptor
|
|
266
|
+
changed = True
|
|
267
|
+
|
|
268
|
+
self._resolved = staged_resolved
|
|
269
|
+
self._path_index = staged_index
|
|
270
|
+
if not changed:
|
|
271
|
+
return current
|
|
272
|
+
candidate = CommonModuleCatalogSnapshot.create(
|
|
273
|
+
profile=current.profile,
|
|
274
|
+
preprocessor_profile=current.preprocessor_profile,
|
|
275
|
+
revision=current.revision + 1,
|
|
276
|
+
modules=staged_modules.values(),
|
|
277
|
+
)
|
|
278
|
+
self._snapshot = candidate
|
|
279
|
+
return candidate
|
|
280
|
+
|
|
281
|
+
def _enumerate_path_index(self) -> dict[str, Path]:
|
|
282
|
+
index: dict[str, Path] = {}
|
|
283
|
+
paths = list(self._common_modules.glob("*.xml"))
|
|
284
|
+
with os.scandir(self._common_modules) as entries:
|
|
285
|
+
for entry in entries:
|
|
286
|
+
if entry.name.casefold().endswith(".xml"):
|
|
287
|
+
continue
|
|
288
|
+
if entry.is_symlink():
|
|
289
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
290
|
+
if not entry.is_dir(follow_symlinks=False):
|
|
291
|
+
continue
|
|
292
|
+
folder = Path(entry.path)
|
|
293
|
+
if folder.is_junction():
|
|
294
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
295
|
+
# One level only: EDT metadata is Name/Name.mdo. Do not scan
|
|
296
|
+
# module source files or descend into Designer's Ext directory.
|
|
297
|
+
for path in folder.glob("*.mdo"):
|
|
298
|
+
if path.stem != folder.name:
|
|
299
|
+
raise ProtocolError("common-module metadata identity is invalid")
|
|
300
|
+
paths.append(path)
|
|
301
|
+
for path in sorted(paths, key=lambda item: item.name.casefold()):
|
|
302
|
+
if not _is_bsl_identifier(path.stem):
|
|
303
|
+
raise ProtocolError("common-module metadata identity is invalid")
|
|
304
|
+
normalized = path.stem.casefold()
|
|
305
|
+
if normalized in index:
|
|
306
|
+
raise ProtocolError("duplicate common module identity")
|
|
307
|
+
index[normalized] = path
|
|
308
|
+
return index
|
|
309
|
+
|
|
310
|
+
def _read_descriptor(self, metadata_path: Path) -> CommonModuleDescriptor | None:
|
|
311
|
+
return self._read_metadata(metadata_path)[1]
|
|
312
|
+
|
|
313
|
+
def _read_metadata(
|
|
314
|
+
self, metadata_path: Path
|
|
315
|
+
) -> tuple[str, CommonModuleDescriptor | None]:
|
|
316
|
+
resolved = self._safe_metadata_path(metadata_path)
|
|
317
|
+
try:
|
|
318
|
+
document = ElementTree.fromstring(resolved.read_bytes())
|
|
319
|
+
except (OSError, ElementTree.ParseError) as error:
|
|
320
|
+
raise ProtocolError("common-module metadata is invalid") from error
|
|
321
|
+
edt = resolved.suffix.casefold() == ".mdo"
|
|
322
|
+
properties = (
|
|
323
|
+
document if _local_name(document.tag) == "CommonModule" else None
|
|
324
|
+
) if edt else _properties_element(document)
|
|
325
|
+
if properties is None:
|
|
326
|
+
raise ProtocolError("common-module metadata is invalid")
|
|
327
|
+
name = _text_property(properties, "name" if edt else "Name")
|
|
328
|
+
if (
|
|
329
|
+
name is None
|
|
330
|
+
or not _is_bsl_identifier(name)
|
|
331
|
+
or resolved.stem != name
|
|
332
|
+
):
|
|
333
|
+
raise ProtocolError("common-module metadata identity is invalid")
|
|
334
|
+
if edt:
|
|
335
|
+
# EDT omits boolean properties at their default false value.
|
|
336
|
+
server = _bool_property(properties, "server", default=False)
|
|
337
|
+
global_module = _bool_property(properties, "global", default=False)
|
|
338
|
+
managed = _bool_property(properties, "clientManagedApplication", default=False)
|
|
339
|
+
ordinary = _bool_property(properties, "clientOrdinaryApplication", default=False)
|
|
340
|
+
client = managed or ordinary
|
|
341
|
+
else:
|
|
342
|
+
server = _bool_property(properties, "Server")
|
|
343
|
+
global_module = _bool_property(properties, "Global")
|
|
344
|
+
client = _bool_property(properties, "ClientManagedApplication") or _bool_property(
|
|
345
|
+
properties, "ClientOrdinaryApplication"
|
|
346
|
+
)
|
|
347
|
+
if not server or global_module:
|
|
348
|
+
return name, None
|
|
349
|
+
return name, CommonModuleDescriptor(
|
|
350
|
+
canonical_name=name,
|
|
351
|
+
scope=(
|
|
352
|
+
CommonModuleScope.CLIENT_SERVER
|
|
353
|
+
if client
|
|
354
|
+
else CommonModuleScope.SERVER
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _safe_metadata_path(self, metadata_path: Path) -> Path:
|
|
359
|
+
path = Path(metadata_path)
|
|
360
|
+
designer_path = path.suffix.casefold() == ".xml" and path.parent == self._common_modules
|
|
361
|
+
edt_path = (
|
|
362
|
+
path.suffix.casefold() == ".mdo"
|
|
363
|
+
and path.parent.parent == self._common_modules
|
|
364
|
+
and path.stem == path.parent.name
|
|
365
|
+
)
|
|
366
|
+
if _is_link(path) or _is_link(path.parent) or not (designer_path or edt_path):
|
|
367
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
368
|
+
try:
|
|
369
|
+
resolved = path.resolve(strict=True)
|
|
370
|
+
except (FileNotFoundError, OSError) as error:
|
|
371
|
+
raise ProtocolError("common-module source root is unsafe") from error
|
|
372
|
+
if resolved.parent != path.parent or not resolved.is_file():
|
|
373
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
374
|
+
return resolved
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _local_name(tag: object) -> str:
|
|
378
|
+
if not isinstance(tag, str):
|
|
379
|
+
return ""
|
|
380
|
+
return tag.rsplit("}", 1)[-1]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _properties_element(document: ElementTree.Element) -> ElementTree.Element | None:
|
|
384
|
+
for common_module in document.iter():
|
|
385
|
+
if _local_name(common_module.tag) != "CommonModule":
|
|
386
|
+
continue
|
|
387
|
+
for child in common_module:
|
|
388
|
+
if _local_name(child.tag) == "Properties":
|
|
389
|
+
return child
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _text_property(properties: ElementTree.Element, name: str) -> str | None:
|
|
394
|
+
values = [child.text or "" for child in properties if _local_name(child.tag) == name]
|
|
395
|
+
if len(values) > 1:
|
|
396
|
+
raise ProtocolError("common-module metadata is invalid")
|
|
397
|
+
return values[0] if values else None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _bool_property(
|
|
401
|
+
properties: ElementTree.Element, name: str, *, default: bool | None = None,
|
|
402
|
+
) -> bool:
|
|
403
|
+
value = _text_property(properties, name)
|
|
404
|
+
if value is None:
|
|
405
|
+
if default is not None:
|
|
406
|
+
return default
|
|
407
|
+
raise ProtocolError("common-module metadata is invalid")
|
|
408
|
+
if value.strip().casefold() == "true":
|
|
409
|
+
return True
|
|
410
|
+
if value.strip().casefold() == "false":
|
|
411
|
+
return False
|
|
412
|
+
raise ProtocolError("common-module metadata is invalid")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _is_link(path: Path) -> bool:
|
|
416
|
+
return path.is_symlink() or path.is_junction()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _is_bsl_identifier(value: object) -> bool:
|
|
420
|
+
return isinstance(value, str) and bool(_BSL_IDENTIFIER_RE.fullmatch(value))
|