Harden RF handoff and stream OTA decode
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user