Harden RF handoff and stream OTA decode

This commit is contained in:
LittleSam129
2026-09-22 17:43:58 +03:00
parent 168e03d9e7
commit a94f2db4ee
17 changed files with 1548 additions and 497 deletions

View File

@@ -14,8 +14,9 @@ from typing import Any, Mapping
SCHEMA_VERSION = 1
EXPECTED_RX_TASK_ID = "RF001A_OTA_CAPTURE_003"
EXPECTED_TX_TASK_ID = "RF001A_OTA_TX_003"
EXPECTED_RX_TASK_ID = "RF001A_OTA_CAPTURE_004"
EXPECTED_TX_TASK_ID = "RF001A_OTA_TX_004"
EXPECTED_CAPTURE_DURATION_SECONDS = 60.0
CHANNEL_TEST_TASK_ID = "RF_HANDOFF_CHANNEL_TEST_001"
READY_STATE = "RX_READY_FOR_TX"
COMPLETE_STATE = "CAPTURE_COMPLETE"
@@ -25,6 +26,21 @@ RESULT_PATHS = frozenset((RESULT_JSON_PATH, RESULT_MARKDOWN_PATH))
MAX_RESULT_BYTES = 64 * 1024
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
_HEAD_RE = re.compile(r"^[0-9a-f]{40}$")
_TRANSIENT_PUSH_ERRORS = (
"authentication failed",
"failed to authenticate",
"could not resolve host",
"connection reset",
"connection timed out",
"failed to connect",
"remote end hung up",
"rpc failed",
"schannel",
"ssl",
"tls",
"the requested url returned error: 5",
"unable to access",
)
class ResultChannelError(RuntimeError):
@@ -319,7 +335,11 @@ def evaluate_ready(
expected_end = _parse_utc(result.expected_capture_end, "expected_capture_end")
published = _parse_utc(result.published_at, "published_at")
planned_duration = (expected_end - started).total_seconds()
if not 119.0 <= planned_duration <= 121.0:
if not (
EXPECTED_CAPTURE_DURATION_SECONDS - 1.0
<= planned_duration
<= EXPECTED_CAPTURE_DURATION_SECONDS + 1.0
):
return ReadyEvaluation(False, "WRONG_CAPTURE_WINDOW")
age = (now_utc - published).total_seconds()
if age < -5.0:
@@ -386,6 +406,32 @@ class GitResultChannel:
def branch_exists(self) -> bool:
return self.remote_head() is not None
def verify_write_access(self) -> str:
"""Проверить право записи dry-run push без коммита и изменения ветки."""
remote_head = self.fetch()
dry_run = self._run(
[
"push",
"--dry-run",
"--porcelain",
self.remote,
f"{remote_head}:refs/heads/{self.branch}",
],
check=False,
)
if dry_run.returncode != 0:
raise ResultChannelError(
"authenticated result-branch push preflight failed: "
f"{dry_run.stderr.strip()}"
)
confirmed_head = self.remote_head()
if confirmed_head != remote_head:
raise ResultChannelError(
"result branch changed during write preflight"
)
return remote_head
def _tracking_reference(self) -> str:
return f"refs/remotes/{self.remote}/{self.branch}"
@@ -553,6 +599,66 @@ class GitResultChannel:
return commit
return None
@staticmethod
def _is_transient_push_error(stderr: str) -> bool:
normalized = stderr.casefold()
return any(pattern in normalized for pattern in _TRANSIENT_PUSH_ERRORS)
def _push_exact_commit(
self,
checkout: Path,
commit: str,
remote_before: str,
) -> str:
"""Отправить неизменный коммит с одной безопасной повторной попыткой."""
push_arguments = [
"push",
"--quiet",
self.remote,
f"{commit}:refs/heads/{self.branch}",
]
first = self._run(push_arguments, cwd=checkout, check=False)
if first.returncode == 0:
remote_after = self.remote_head()
if remote_after != commit:
raise ResultChannelError(
"remote result HEAD does not confirm the published commit"
)
return commit
observed_after_failure = self.remote_head()
if observed_after_failure == commit:
return commit
if observed_after_failure != remote_before:
raise ResultChannelError(
"result push failed and remote branch changed to a conflicting "
f"commit: {remote_before} -> {observed_after_failure}"
)
if not self._is_transient_push_error(first.stderr):
raise ResultChannelError(f"result push failed: {first.stderr.strip()}")
fetched_head = self.fetch()
if fetched_head != remote_before:
raise ResultChannelError(
"remote result branch changed before the single retry"
)
second = self._run(push_arguments, cwd=checkout, check=False)
if second.returncode != 0:
observed_after_retry = self.remote_head()
if observed_after_retry == commit:
return commit
raise ResultChannelError(
"result push failed after one retry of the same commit: "
f"{second.stderr.strip()}"
)
remote_after = self.remote_head()
if remote_after != commit:
raise ResultChannelError(
"remote result HEAD does not confirm the retried commit"
)
return commit
def publish(self, result: RxHandoffResult) -> PublishOutcome:
validate_result(result)
remote_before = self.fetch()
@@ -610,25 +716,11 @@ class GitResultChannel:
cwd=checkout,
)
commit = self._run(["rev-parse", "HEAD"], cwd=checkout).stdout.strip()
push = self._run(
[
"push",
"--quiet",
self.remote,
f"HEAD:refs/heads/{self.branch}",
],
cwd=checkout,
check=False,
remote_after = self._push_exact_commit(
checkout,
commit,
remote_before,
)
if push.returncode != 0:
raise ResultChannelError(
f"result push failed: {push.stderr.strip()}"
)
remote_after = self.remote_head()
if remote_after is None or remote_after != commit:
raise ResultChannelError(
"remote result HEAD does not confirm the published commit"
)
return PublishOutcome("PUBLISHED", commit, remote_after)
finally:
if worktree_added: