better-dsh 0.3.0 → 0.3.1-a
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/README.md +49 -177
- package/cordis.patch.yml +5 -36
- package/docs/20_specs/kernel-provisioning/spec.md +2 -0
- package/docs/adr/0015-independent-bundles-agent-remote.md +40 -0
- package/docs/remote-tools.md +56 -0
- package/docs/specs/kernel-provisioning/spec.md +12 -40
- package/docs/specs/omp-python-repl-port/runtime-guide.md +35 -0
- package/docs/specs/omp-python-repl-port/spec.md +68 -0
- package/docs/specs/remote-tool-enhancement/spec.md +203 -0
- package/dsh-docs/baseline.md +13 -0
- package/dsh-docs/subsystems/invariants.md +3 -0
- package/dsh-docs/subsystems/invariants.zh.md +3 -0
- package/eval-description.md +7 -3
- package/lib/LaunchOptions-0Mr6meJc.js +4 -0
- package/lib/{LaunchOptions-kGXcZOe0.js → LaunchOptions-iE9M-STS.js} +1 -1
- package/lib/{bidi-BalpKaJc.js → bidi-CaQeBZHY.js} +1 -1
- package/lib/client/index.js +30 -6
- package/lib/failover/index.js +1 -1
- package/lib/fs-aware/sandbox-plugin.js +3 -3
- package/lib/{index-browser-CysrjMyM.js → index-browser-CJzxT9Vi.js} +4 -4
- package/lib/index.d.ts +16 -50
- package/lib/index.js +482 -1720
- package/lib/kernel-env-x_WkG6Fr.js +76 -0
- package/lib/kernel-env.d.ts +5 -69
- package/lib/kernel-env.js +2 -2
- package/lib/{lsp-server-registry-BXvUTBY7.js → lsp-server-registry-BcdkHROD.js} +1 -1
- package/lib/{lsp-server-registry-CH9WNFFt.js → lsp-server-registry-D4VLs1Sm.js} +1 -1
- package/lib/{main-BQeFrBM1.js → main-taSMz1KX.js} +1 -1
- package/lib/{main-CupOhMp5.js → main-uqGqYYnA.js} +7 -5
- package/lib/mobile/plugin.d.ts +1 -1
- package/lib/omp-python/DILL-LICENSE +35 -0
- package/lib/omp-python/OMP-LICENSE +23 -0
- package/lib/omp-python/better_snapshot.py +391 -0
- package/lib/omp-python/better_support.py +425 -0
- package/lib/omp-python/dill-provenance.json +55 -0
- package/lib/omp-python/provenance.json +13 -0
- package/lib/omp-python/runner.py +2499 -0
- package/lib/omp-python/vendor/dill/__diff.py +234 -0
- package/lib/omp-python/vendor/dill/__info__.py +291 -0
- package/lib/omp-python/vendor/dill/__init__.py +119 -0
- package/lib/omp-python/vendor/dill/_dill.py +2265 -0
- package/lib/omp-python/vendor/dill/_objects.py +545 -0
- package/lib/omp-python/vendor/dill/_shims.py +193 -0
- package/lib/omp-python/vendor/dill/detect.py +287 -0
- package/lib/omp-python/vendor/dill/logger.py +285 -0
- package/lib/omp-python/vendor/dill/objtypes.py +24 -0
- package/lib/omp-python/vendor/dill/pointers.py +122 -0
- package/lib/omp-python/vendor/dill/session.py +612 -0
- package/lib/omp-python/vendor/dill/settings.py +25 -0
- package/lib/omp-python/vendor/dill/source.py +1025 -0
- package/lib/omp-python/vendor/dill/temp.py +252 -0
- package/lib/{puppeteer-core-CSk2PtSm.js → puppeteer-core-C-4NyOpY.js} +8 -8
- package/lib/remote/plugin.d.ts +459 -5
- package/lib/remote/plugin.js +2389 -210
- package/lib/url-schemes/index.d.ts +11 -11
- package/lib/url-schemes/index.js +11 -11
- package/lib/web-password.d.ts +8 -8
- package/lib/web-password.js +5 -1
- package/lib/{web-trust-Dej_RI2B.d.ts → web-trust-C_Y6o8Z2.d.ts} +11 -11
- package/lib/web-trust.d.ts +1 -2
- package/lib/welcome-notice.d.ts +5 -7
- package/lib/welcome-notice.js +9 -48
- package/lib/{wrap-T4I0nfPM.js → wrap-0VA0uFx8.js} +1 -1
- package/package.json +7 -24
- package/scripts/kernel-check.mjs +5 -0
- package/lib/LaunchOptions-B874eBu2.js +0 -4
- package/lib/bundle-delivery.d.ts +0 -20
- package/lib/bundle-delivery.js +0 -209
- package/lib/hmr.d.ts +0 -32
- package/lib/hmr.js +0 -49
- package/lib/kernel-bridge.py +0 -172
- package/lib/kernel-env-D8UOWeji.js +0 -195
- package/lib/webserver.d.ts +0 -10
- package/lib/webserver.js +0 -19
- package/scripts/kernel-provision.mjs +0 -35
- package/scripts/register-bundle.mjs +0 -80
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Best-effort checkpoints for the owned OMP namespace; dill 0.4.1 adapter.
|
|
2
|
+
|
|
3
|
+
No cell history is stored/replayed. Pickling user objects is not a sandbox:
|
|
4
|
+
reducers may execute arbitrary code. Host deadlines bound helper execution.
|
|
5
|
+
"""
|
|
6
|
+
import _thread
|
|
7
|
+
import asyncio
|
|
8
|
+
import dis
|
|
9
|
+
import functools
|
|
10
|
+
import hashlib
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import re
|
|
16
|
+
import socket
|
|
17
|
+
import sqlite3
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import types
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
# Select the unchanged packaged implementation before ANY user cell can import dill.
|
|
26
|
+
_VENDOR = os.path.join(os.path.dirname(__file__), 'vendor')
|
|
27
|
+
sys.path.insert(0, _VENDOR)
|
|
28
|
+
import dill
|
|
29
|
+
if dill.__version__ != '0.4.1' or os.path.realpath(dill.__file__) != os.path.realpath(os.path.join(_VENDOR, 'dill', '__init__.py')):
|
|
30
|
+
raise RuntimeError('Better requires its packaged dill 0.4.1')
|
|
31
|
+
|
|
32
|
+
_DIRECTORY = None
|
|
33
|
+
_CAP = None
|
|
34
|
+
_TIMEOUT = 30.0
|
|
35
|
+
_FORMAT = 2
|
|
36
|
+
_RESOURCES = (io.IOBase, _thread.LockType, type(threading.RLock()), socket.socket,
|
|
37
|
+
subprocess.Popen, threading.Thread, sqlite3.Connection, sqlite3.Cursor,
|
|
38
|
+
asyncio.Future, asyncio.AbstractEventLoop, types.GeneratorType,
|
|
39
|
+
types.CoroutineType, types.AsyncGeneratorType)
|
|
40
|
+
_PRIMITIVES = (str, bytes, int, float, bool, type(None))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UnsupportedResource(Exception):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SizeCap(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NamespaceProxy:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _proxy(ns):
|
|
56
|
+
proxy = NamespaceProxy()
|
|
57
|
+
proxy.__dict__ = ns
|
|
58
|
+
return proxy
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def configure(directory, cap, timeout_ms):
|
|
62
|
+
global _DIRECTORY, _CAP, _TIMEOUT
|
|
63
|
+
_DIRECTORY = os.path.abspath(directory) if directory else None
|
|
64
|
+
_CAP = cap
|
|
65
|
+
_TIMEOUT = timeout_ms / 1000.0 * 0.9
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clear():
|
|
69
|
+
"""Delete only our fixed manifest and generation files, including orphans."""
|
|
70
|
+
if _DIRECTORY and os.path.islink(_DIRECTORY):
|
|
71
|
+
raise ValueError('checkpoint directory cannot be a symlink')
|
|
72
|
+
if not _DIRECTORY or not os.path.isdir(_DIRECTORY):
|
|
73
|
+
return
|
|
74
|
+
for name in os.listdir(_DIRECTORY):
|
|
75
|
+
if name == 'manifest.json' or re.fullmatch(r'(state-[0-9a-f]{32}\.dill|\.checkpoint-[0-9a-f]{32}\.tmp)', name):
|
|
76
|
+
os.unlink(os.path.join(_DIRECTORY, name))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _paths(payload_path, manifest_path):
|
|
80
|
+
if not _DIRECTORY or os.path.abspath(manifest_path) != os.path.join(_DIRECTORY, 'manifest.json') or os.path.abspath(payload_path) != os.path.join(_DIRECTORY, 'state.dill'):
|
|
81
|
+
raise ValueError('checkpoint paths are not the configured session directory')
|
|
82
|
+
os.makedirs(_DIRECTORY, mode=0o700, exist_ok=True)
|
|
83
|
+
if os.path.islink(_DIRECTORY):
|
|
84
|
+
raise ValueError('checkpoint directory cannot be a symlink')
|
|
85
|
+
if os.path.islink(manifest_path):
|
|
86
|
+
raise ValueError('checkpoint manifest cannot be a symlink')
|
|
87
|
+
os.chmod(_DIRECTORY, 0o700)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _environment():
|
|
91
|
+
return dict(pythonVersion=platform.python_version(), implementation=platform.python_implementation(),
|
|
92
|
+
interpreter=os.path.realpath(sys.executable), venvPath=sys.prefix,
|
|
93
|
+
environmentHash=hashlib.sha256(json.dumps([sys.prefix, sys.base_prefix, sys.path], sort_keys=True).encode()).hexdigest(),
|
|
94
|
+
dillVersion=dill.__version__, protocol=4, skills=[])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _guard(ns):
|
|
98
|
+
import better_support
|
|
99
|
+
owned = [sys.modules['__main__'], sys.modules['better_support'], sys.modules[__name__]]
|
|
100
|
+
owned.extend(better_support.owned_objects())
|
|
101
|
+
denied = {id(value) for value in owned if not isinstance(value, _PRIMITIVES)}
|
|
102
|
+
def guard(value):
|
|
103
|
+
if id(value) in denied or (isinstance(value, (types.FunctionType, type)) and getattr(value, '__module__', None) in ('better_support', 'better_snapshot')) or type(value).__module__ == 'better_support':
|
|
104
|
+
raise UnsupportedResource('captured Better/runner binding')
|
|
105
|
+
if isinstance(value, _RESOURCES):
|
|
106
|
+
raise UnsupportedResource('unsupported live resource ' + type(value).__name__)
|
|
107
|
+
return None
|
|
108
|
+
return guard
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class CappedBuffer(io.BytesIO):
|
|
112
|
+
def __init__(self, cap):
|
|
113
|
+
super().__init__()
|
|
114
|
+
self.cap = cap
|
|
115
|
+
def write(self, value):
|
|
116
|
+
if self.tell() + len(value) > self.cap:
|
|
117
|
+
raise SizeCap('serialized graph exceeds snapshotSizeCapBytes')
|
|
118
|
+
return super().write(value)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _encode(ns, values, cap, guard):
|
|
122
|
+
output = CappedBuffer(cap)
|
|
123
|
+
# Exact Pickler: subclassing disables pinned dill's main-module logic.
|
|
124
|
+
pickler = dill.Pickler(output, protocol=4, recurse=False, byref=False)
|
|
125
|
+
pickler._main = _proxy(ns)
|
|
126
|
+
pickler._original_main = pickler._main
|
|
127
|
+
pickler._main_modified = True
|
|
128
|
+
pickler.persistent_id = guard
|
|
129
|
+
pickler.dump(values)
|
|
130
|
+
return output.getvalue()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _decode(ns, payload):
|
|
134
|
+
unpickler = dill.Unpickler(io.BytesIO(payload))
|
|
135
|
+
unpickler._main = _proxy(ns)
|
|
136
|
+
unpickler._ignore = True
|
|
137
|
+
return unpickler.load()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _dependencies(root, ns):
|
|
141
|
+
"""Bounded static global scan; dynamic eval/globals lookups remain best effort."""
|
|
142
|
+
found, seen = set(), set()
|
|
143
|
+
def walk(value):
|
|
144
|
+
if id(value) in seen or isinstance(value, _PRIMITIVES):
|
|
145
|
+
return
|
|
146
|
+
seen.add(id(value))
|
|
147
|
+
if len(seen) > 10000:
|
|
148
|
+
raise UnsupportedResource('dependency graph exceeds inspection budget')
|
|
149
|
+
if isinstance(value, types.FunctionType):
|
|
150
|
+
if value.__globals__ is ns:
|
|
151
|
+
def code_names(code):
|
|
152
|
+
for op in dis.get_instructions(code):
|
|
153
|
+
if op.opname in ('LOAD_GLOBAL', 'LOAD_NAME'):
|
|
154
|
+
found.add(op.argval)
|
|
155
|
+
for item in code.co_consts:
|
|
156
|
+
if isinstance(item, types.CodeType): code_names(item)
|
|
157
|
+
code_names(value.__code__)
|
|
158
|
+
walk(value.__defaults__)
|
|
159
|
+
walk(value.__kwdefaults__)
|
|
160
|
+
for cell in value.__closure__ or ():
|
|
161
|
+
try: walk(cell.cell_contents)
|
|
162
|
+
except ValueError: pass
|
|
163
|
+
elif isinstance(value, functools.partial):
|
|
164
|
+
walk(value.func); walk(value.args); walk(value.keywords); walk(vars(value))
|
|
165
|
+
elif isinstance(value, (staticmethod, classmethod)):
|
|
166
|
+
walk(value.__func__)
|
|
167
|
+
elif isinstance(value, property):
|
|
168
|
+
walk(value.fget); walk(value.fset); walk(value.fdel)
|
|
169
|
+
elif isinstance(value, types.MethodType):
|
|
170
|
+
walk(value.__func__); walk(value.__self__)
|
|
171
|
+
elif isinstance(value, dict):
|
|
172
|
+
for key, item in value.items(): walk(key); walk(item)
|
|
173
|
+
elif isinstance(value, (tuple, list, set, frozenset)):
|
|
174
|
+
for item in value: walk(item)
|
|
175
|
+
elif isinstance(value, type):
|
|
176
|
+
if value.__module__ == ns.get('__name__'):
|
|
177
|
+
for item in vars(value).values(): walk(item)
|
|
178
|
+
for base in value.__bases__: walk(base)
|
|
179
|
+
elif not isinstance(value, types.ModuleType) and type(value).__module__ == ns.get('__name__'):
|
|
180
|
+
walk(type(value))
|
|
181
|
+
if hasattr(value, '__dict__'): walk(vars(value))
|
|
182
|
+
# Scan descriptors across the MRO, including inherited/mangled slots.
|
|
183
|
+
for owner in type(value).__mro__:
|
|
184
|
+
for descriptor in vars(owner).values():
|
|
185
|
+
if isinstance(descriptor, types.MemberDescriptorType):
|
|
186
|
+
try: walk(descriptor.__get__(value, type(value)))
|
|
187
|
+
except AttributeError: pass # uninitialized slot
|
|
188
|
+
walk(root)
|
|
189
|
+
return found
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _write_new(path, data):
|
|
193
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
194
|
+
with os.fdopen(fd, 'wb') as output:
|
|
195
|
+
output.write(data)
|
|
196
|
+
output.flush()
|
|
197
|
+
os.fsync(output.fileno())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _emit(sentinel, result):
|
|
201
|
+
print(sentinel + json.dumps(result, ensure_ascii=True))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _retained_payloads(manifest_path):
|
|
205
|
+
try:
|
|
206
|
+
with open(manifest_path, encoding='utf-8') as source:
|
|
207
|
+
manifest = json.load(source)
|
|
208
|
+
except (FileNotFoundError, ValueError):
|
|
209
|
+
return set(), None
|
|
210
|
+
if not isinstance(manifest, dict):
|
|
211
|
+
return set(), None
|
|
212
|
+
current = manifest.get('payload')
|
|
213
|
+
names = (current, manifest.get('previousPayload'))
|
|
214
|
+
retained = {name for name in names if isinstance(name, str) and re.fullmatch(r'state-[0-9a-f]{32}\.dill', name)}
|
|
215
|
+
return retained, current if isinstance(current, str) and current in retained else None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _scavenge(retained):
|
|
219
|
+
for name in os.listdir(_DIRECTORY):
|
|
220
|
+
if name not in retained and re.fullmatch(r'(state-[0-9a-f]{32}\.dill|\.checkpoint-[0-9a-f]{32}\.tmp)', name):
|
|
221
|
+
os.unlink(os.path.join(_DIRECTORY, name))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def save(ns, payload_path, manifest_path, spec_json, sentinel):
|
|
225
|
+
attempt_paths, committed, result = [], False, None
|
|
226
|
+
payload_name, manifest, retained = None, None, set()
|
|
227
|
+
try:
|
|
228
|
+
_paths(payload_path, manifest_path)
|
|
229
|
+
retained, previous = _retained_payloads(manifest_path)
|
|
230
|
+
_scavenge(retained) # collect killed-writer artifacts even if this attempt fails
|
|
231
|
+
deadline = time.monotonic() + _TIMEOUT
|
|
232
|
+
def check_deadline():
|
|
233
|
+
if time.monotonic() >= deadline:
|
|
234
|
+
raise TimeoutError('checkpoint deadline exceeded; previous generation retained')
|
|
235
|
+
spec = json.loads(spec_json)
|
|
236
|
+
cap = min(spec['sizeCapBytes'], _CAP)
|
|
237
|
+
import better_support
|
|
238
|
+
candidates = better_support.user_namespace(ns)
|
|
239
|
+
guard = _guard(ns)
|
|
240
|
+
kept, omitted, dependencies = {}, {}, {}
|
|
241
|
+
for name, value in candidates.items():
|
|
242
|
+
check_deadline()
|
|
243
|
+
try:
|
|
244
|
+
_encode(ns, {name: value}, cap, guard)
|
|
245
|
+
dependencies[name] = _dependencies(value, ns)
|
|
246
|
+
kept[name] = value
|
|
247
|
+
except SizeCap:
|
|
248
|
+
raise
|
|
249
|
+
except Exception as error:
|
|
250
|
+
omitted[name] = type(error).__name__ + ': ' + str(error)
|
|
251
|
+
changed = True
|
|
252
|
+
while changed:
|
|
253
|
+
changed = False
|
|
254
|
+
for name in list(kept):
|
|
255
|
+
missing = dependencies[name].intersection(omitted)
|
|
256
|
+
if missing:
|
|
257
|
+
omitted[name] = 'depends on omitted globals: ' + ', '.join(sorted(missing))
|
|
258
|
+
del kept[name]
|
|
259
|
+
changed = True
|
|
260
|
+
check_deadline()
|
|
261
|
+
payload = _encode(ns, kept, cap, guard)
|
|
262
|
+
# Verify the FINAL shared graph, never independently reconstruct values.
|
|
263
|
+
trial_ns = dict(ns)
|
|
264
|
+
trial = _decode(trial_ns, payload)
|
|
265
|
+
if not isinstance(trial, dict) or set(trial) != set(kept):
|
|
266
|
+
raise ValueError('final graph round-trip changed admitted names')
|
|
267
|
+
trial_ns.update(trial)
|
|
268
|
+
groups = {}
|
|
269
|
+
for name, value in kept.items():
|
|
270
|
+
if id(value) in groups and trial[name] is not trial[groups[id(value)]]:
|
|
271
|
+
raise ValueError('final graph round-trip lost shared root identity')
|
|
272
|
+
groups[id(value)] = name
|
|
273
|
+
token = uuid.uuid4().hex
|
|
274
|
+
payload_name = 'state-' + token + '.dill'
|
|
275
|
+
manifest = dict(_environment(), snapshotFormat=_FORMAT, turn=spec['turn'],
|
|
276
|
+
payload=payload_name, previousPayload=previous, sizeBytes=len(payload), sha256=hashlib.sha256(payload).hexdigest(),
|
|
277
|
+
names=sorted(kept), dependencies={name: sorted(dependencies[name]) for name in kept},
|
|
278
|
+
bindingNames=sorted(better_support.injected_names()), omitted=omitted, coverage='partial' if omitted else 'best-effort',
|
|
279
|
+
skipped=False, limitations=['Dynamic globals/eval and arbitrary extension resources cannot be exhaustively inspected.'])
|
|
280
|
+
payload_file = os.path.join(_DIRECTORY, payload_name)
|
|
281
|
+
attempt_paths.append(payload_file)
|
|
282
|
+
_write_new(payload_file, payload)
|
|
283
|
+
temp = os.path.join(_DIRECTORY, '.checkpoint-' + token + '.tmp')
|
|
284
|
+
attempt_paths.append(temp)
|
|
285
|
+
_write_new(temp, json.dumps(manifest, sort_keys=True).encode())
|
|
286
|
+
# Sole commit point. Cleanup must never remove this payload afterwards.
|
|
287
|
+
check_deadline()
|
|
288
|
+
os.replace(temp, manifest_path)
|
|
289
|
+
committed = True
|
|
290
|
+
result = dict(kind='ok', sizeBytes=len(payload), names=manifest['names'], omitted=omitted, turn=spec['turn'])
|
|
291
|
+
warnings = []
|
|
292
|
+
if os.name == 'posix':
|
|
293
|
+
try:
|
|
294
|
+
fd = os.open(_DIRECTORY, os.O_RDONLY)
|
|
295
|
+
try: os.fsync(fd)
|
|
296
|
+
finally: os.close(fd)
|
|
297
|
+
except OSError as error:
|
|
298
|
+
warnings.append('directory fsync failed: ' + str(error))
|
|
299
|
+
try:
|
|
300
|
+
_scavenge({payload_name, previous})
|
|
301
|
+
except OSError as error:
|
|
302
|
+
warnings.append('stale artifact cleanup failed: ' + str(error))
|
|
303
|
+
if warnings:
|
|
304
|
+
result['warning'] = 'checkpoint committed; ' + '; '.join(warnings)
|
|
305
|
+
except SizeCap as error:
|
|
306
|
+
result = dict(kind='skipped', reason=str(error), capBytes=_CAP)
|
|
307
|
+
except Exception as error:
|
|
308
|
+
reason = type(error).__name__ + ': ' + str(error)
|
|
309
|
+
if payload_name is not None and not committed:
|
|
310
|
+
try:
|
|
311
|
+
_, pointed = _retained_payloads(manifest_path)
|
|
312
|
+
committed = pointed == payload_name
|
|
313
|
+
except OSError:
|
|
314
|
+
pass
|
|
315
|
+
if committed:
|
|
316
|
+
result = dict(kind='ok', sizeBytes=len(payload), names=manifest['names'], omitted=omitted, turn=spec['turn'])
|
|
317
|
+
result['warning'] = 'checkpoint committed; postcommit finalization failed: ' + reason
|
|
318
|
+
else:
|
|
319
|
+
result = dict(kind='failed', reason=reason)
|
|
320
|
+
finally:
|
|
321
|
+
if not committed and attempt_paths:
|
|
322
|
+
# A signal/error can arrive immediately after replace, before the
|
|
323
|
+
# local flag changes. The pointer is authoritative for deletion.
|
|
324
|
+
try:
|
|
325
|
+
latest, _ = _retained_payloads(manifest_path)
|
|
326
|
+
retained.update(latest)
|
|
327
|
+
except OSError:
|
|
328
|
+
# Cannot prove non-commit: leave recognized artifacts for the
|
|
329
|
+
# next save's pre-write scavenger rather than risk data loss.
|
|
330
|
+
retained.update(os.path.basename(path) for path in attempt_paths)
|
|
331
|
+
for path in attempt_paths:
|
|
332
|
+
if os.path.basename(path) in retained:
|
|
333
|
+
continue
|
|
334
|
+
try: os.unlink(path)
|
|
335
|
+
except FileNotFoundError: pass
|
|
336
|
+
except OSError as error:
|
|
337
|
+
if result is not None:
|
|
338
|
+
result['reason'] += '; uncommitted artifact cleanup failed: ' + str(error)
|
|
339
|
+
if result is not None:
|
|
340
|
+
_emit(sentinel, result)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def restore(ns, payload_path, manifest_path, sentinel, bindings_json):
|
|
344
|
+
try:
|
|
345
|
+
_paths(payload_path, manifest_path)
|
|
346
|
+
if os.path.getsize(manifest_path) > 1048576:
|
|
347
|
+
raise ValueError('manifest exceeds metadata cap')
|
|
348
|
+
with open(manifest_path, encoding='utf-8') as source:
|
|
349
|
+
manifest = json.load(source)
|
|
350
|
+
if not isinstance(manifest, dict) or manifest.get('snapshotFormat') != _FORMAT:
|
|
351
|
+
raise ValueError('unsupported snapshot format')
|
|
352
|
+
for key, value in _environment().items():
|
|
353
|
+
if manifest.get(key) != value: raise ValueError('checkpoint environment mismatch: ' + key)
|
|
354
|
+
name = manifest.get('payload', '')
|
|
355
|
+
if not isinstance(name, str) or not re.fullmatch(r'state-[0-9a-f]{32}\.dill', name):
|
|
356
|
+
raise ValueError('invalid owned payload name')
|
|
357
|
+
names = manifest.get('names')
|
|
358
|
+
import better_support
|
|
359
|
+
if not isinstance(names, list) or any(not isinstance(n, str) or n.startswith('__omp_') or n in ('__name__', '__builtins__', '__doc__') for n in names):
|
|
360
|
+
raise ValueError('invalid checkpoint namespace names')
|
|
361
|
+
turn = manifest.get('turn')
|
|
362
|
+
if not isinstance(turn, int) or isinstance(turn, bool) or turn < 0:
|
|
363
|
+
raise ValueError('invalid checkpoint turn')
|
|
364
|
+
path = os.path.join(_DIRECTORY, name)
|
|
365
|
+
size = os.path.getsize(path)
|
|
366
|
+
if os.path.islink(path) or size != manifest.get('sizeBytes') or size > _CAP:
|
|
367
|
+
raise ValueError('payload size/cap/path validation failed')
|
|
368
|
+
with open(path, 'rb') as source: payload = source.read(_CAP + 1)
|
|
369
|
+
if hashlib.sha256(payload).hexdigest() != manifest.get('sha256'):
|
|
370
|
+
raise ValueError('payload integrity hash mismatch')
|
|
371
|
+
values = _decode(ns, payload)
|
|
372
|
+
if not isinstance(values, dict) or sorted(values) != sorted(names):
|
|
373
|
+
raise ValueError('payload namespace differs from manifest')
|
|
374
|
+
omitted = dict(manifest.get('omitted', {}))
|
|
375
|
+
unavailable = set(manifest.get('bindingNames', [])) - set(json.loads(bindings_json))
|
|
376
|
+
dependencies = manifest.get('dependencies', {})
|
|
377
|
+
changed = True
|
|
378
|
+
while changed:
|
|
379
|
+
changed = False
|
|
380
|
+
for root in list(values):
|
|
381
|
+
missing_bindings = set(dependencies.get(root, [])).intersection(unavailable)
|
|
382
|
+
missing_state = set(dependencies.get(root, [])).intersection(omitted)
|
|
383
|
+
if missing_bindings or missing_state:
|
|
384
|
+
omitted[root] = ('missing current host binding: ' + ', '.join(sorted(missing_bindings))) if missing_bindings else ('depends on omitted globals: ' + ', '.join(sorted(missing_state)))
|
|
385
|
+
del values[root]
|
|
386
|
+
changed = True
|
|
387
|
+
ns.update(values)
|
|
388
|
+
_emit(sentinel, dict(kind='restored', count=len(values), turn=turn, names=names,
|
|
389
|
+
omitted=omitted))
|
|
390
|
+
except Exception as error:
|
|
391
|
+
_emit(sentinel, dict(kind='degraded', reason=type(error).__name__ + ': ' + str(error)))
|