refactor(file_open): PR 14.5 — atomic write helper (H1 first-PR scope)
PYTHON_THINNING_PLAN §5 PR 14.5. BACKLOG H1 first-PR scope의 *전제* —
full Rust transaction (read+guard+write 한 함수)는 PR 14.5b 후속.
문제:
- 기존 ``open_remote_file_into_local_cache``는 multi-step:
(1) execute_remote_read_file (helper bridge)
(2) evaluate_open_file (guard policy)
(3) parent.mkdir(...)
(4) target.write_bytes(...)
- (4) 도중 인터프리터가 죽으면 ``target``이 *truncated bytes*로 존재.
다른 reader(LSP, ruff 등)가 partial state를 보고 잘못된 결과 반환.
- shipping-operator 4-team 토론 시 v0.6.12 #13/#14 silent corruption
영역으로 지목된 path.
산출물:
- ``_atomic_write_bytes(target, body)`` helper 신설 (~25 LOC):
- tempfile.mkstemp으로 sibling tempfile 생성 (같은 parent → rename
atomic).
- write 후 ``Path.replace``로 atomic rename (POSIX rename(2),
Windows MoveFileEx — 둘 다 same-volume atomic).
- BaseException 시 best-effort tempfile cleanup (signal/error 시
.NAME.XXX.part 잔재 방지).
- ``open_remote_file_into_local_cache`` write phase가 helper 호출로
교체. 다른 단계(read / guard / outcome)는 변경 없음.
H1 first-PR scope 충족:
- 전제 #1: write phase가 partial-state 노출 0. ✅
- 후속 (PR 14.5b): read+guard+write를 *한 Rust 함수*로 (broker request
invocation까지 Rust 측 통합). 회귀 표면 매우 크므로 분할.
테스트: sublime/tests 1313 그린 (test_ssh_file_transport / test_cmd_save /
test_integration_remote_file_ops 121건 비트 동일).
boundary lint 위반 0건.
boundary-claim:
removes:
- sublime/sessions/ssh_file_transport.py:2145-2146 # 비-atomic mkdir+write
delete-count: 2 (atomic-write helper 25줄로 교체)
ban-list: 'H1 first-PR scope 전제 — full Rust transaction PR 14.5b'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2082,6 +2082,40 @@ def execute_remote_write_file(
|
||||
)
|
||||
|
||||
|
||||
def _atomic_write_bytes(target: Path, body: bytes) -> None:
|
||||
"""Write ``body`` to ``target`` atomically (tempfile + rename).
|
||||
|
||||
H1 first-PR scope (PR 14.5): the previous flow did
|
||||
``parent.mkdir(...); target.write_bytes(...)`` — a partial-state
|
||||
window where ``target`` could exist with truncated bytes if the
|
||||
interpreter died between ``open()`` and ``close()``. Writing to a
|
||||
sibling tempfile and atomically renaming closes that window: any
|
||||
observer either sees the *prior* contents (or absence) or the
|
||||
*complete* new bytes, never a partial.
|
||||
"""
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Same parent so ``rename`` is a same-filesystem atomic op (POSIX
|
||||
# ``rename(2)``; on Windows ``Path.replace`` falls back to
|
||||
# ``MoveFileEx`` which is atomic for same-volume targets).
|
||||
fd, tmp_str = tempfile.mkstemp(
|
||||
prefix="." + target.name + ".", suffix=".part", dir=str(target.parent)
|
||||
)
|
||||
tmp_path = Path(tmp_str)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as fh:
|
||||
fh.write(body)
|
||||
tmp_path.replace(target)
|
||||
except BaseException:
|
||||
# On any failure (write error, signal, etc.) do best-effort cleanup
|
||||
# of the tempfile so the cache directory does not accumulate
|
||||
# ``.NAME.XXXXXX.part`` debris.
|
||||
try:
|
||||
tmp_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def open_remote_file_into_local_cache(
|
||||
host_alias: str,
|
||||
*,
|
||||
@@ -2092,6 +2126,11 @@ def open_remote_file_into_local_cache(
|
||||
) -> OpenFileResult:
|
||||
"""Fetch remote bytes over SSH, run open guardrails, and write the local cache file.
|
||||
|
||||
Wave 2 PR 14.5 (H1 first-PR scope): write phase uses
|
||||
:func:`_atomic_write_bytes` so a crash between read and write cannot
|
||||
leave a half-written cache file. Full Rust transaction (read +
|
||||
guardrail + write inside one Rust call) lands as PR 14.5b.
|
||||
|
||||
Transport failures are surfaced as ``OpenOutcome.TRANSPORT_ERROR`` so callers
|
||||
can stay UI-free while still distinguishing policy blocks from SSH issues.
|
||||
Missing remote paths (``ENOENT`` / ``lstat_failed``) return
|
||||
@@ -2142,8 +2181,7 @@ def open_remote_file_into_local_cache(
|
||||
)
|
||||
if opened.outcome is not OpenOutcome.OK:
|
||||
return opened
|
||||
local_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
local_cache_path.write_bytes(read_result.body)
|
||||
_atomic_write_bytes(local_cache_path, read_result.body)
|
||||
return OpenFileResult(
|
||||
outcome=opened.outcome,
|
||||
local_cache_path=opened.local_cache_path,
|
||||
|
||||
Reference in New Issue
Block a user