ZaStoGram/Tools/generate_mtproxy_phase_classification.py
loop-uh 8a5c466c82 Refactor MTProxy recorder and emoji animation scheduler
Extract MTProxy endpoint recording logic into MtProxyEndpointRecorder and MtProxyProbeLease helper classes, and introduce EmojiPanelAnimationScheduler for controlled animation playback in emoji panels.

Key changes:
- Move endpoint failure/handshake/data-path recording from ConnectionSocket to MtProxyEndpointRecorder
- Replace probe lease management methods with MtProxyProbeLease member class
- Add EmojiPanelAnimationScheduler to manage GIF/sticker playback based on visibility and scroll state
- Add animation FPS limiting and invalidation delegate hooks to ImageReceiver
- Update Java proxy event handling to route connected/connect_start/usable_success through ProxyEventReducer
- Update verification tools to reflect new architecture
2026-07-02 21:01:55 +03:00

324 lines
12 KiB
Python

#!/usr/bin/env python3
"""Generate TMessagesProj/jni/mtproxy/MtProxyPhaseClassification.h.
Single source of truth for MTProxy phase classification is
Tools/mtproxy_phase_contract.py. This script renders the native classification
helpers from it so C++ never grows hand-maintained parallel strcmp lists again
(the 02.07 reconnect livelock was caused by exactly such a list drifting).
The generated header is checked in. Run without arguments to (re)write it;
run with --check to verify the checked-in header matches the contract
(used by check_mtproxy_all.py).
Every generated phase name must also exist as a constexpr constant in
MtProxyPhaseContract.h — the generator fails otherwise, so the C++ constants
and the Python contract cannot drift silently.
"""
from pathlib import Path
import re
import sys
from mtproxy_phase_contract import (
PHASES,
FACADE_ONLY_PHASES,
PHASE_FAILURE,
PHASE_LIVE,
PHASE_NEUTRAL,
PHASE_SUCCESS,
java_backoff_phases,
java_key_scope_for,
java_phase_names,
java_rotation_phases,
local_scheduler_timeout_phases,
observation_facade_phases,
pre_io_terminal_phases,
reconnect_backoff_phases,
)
ROOT = Path(__file__).resolve().parents[1]
CONTRACT_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseContract.h"
OUTPUT_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseClassification.h"
OUTPUT_JAVA = (
ROOT
/ "TMessagesProj/src/main/java/org/telegram/messenger/ProxyPhaseClassification.java"
)
FUNCTIONS = (
(
"isPreIoTerminalVerdict",
pre_io_terminal_phases,
(
"Terminal verdicts that can be assigned before the socket performs any\n"
"real I/O (secret validation, DNS verdict caches, probe-coordinator\n"
"backoff decisions). deriveMtProxyTerminalDiagnostic must preserve these\n"
"across closeSocket: re-deriving from the startup timeline would replace\n"
"them with \"connection_not_started\", which is on the local-scheduler\n"
"skip list, so neither endpoint cooldown nor reconnect backoff would\n"
"engage and connect() would hot-loop."
),
),
(
"needsReconnectBackoff",
reconnect_backoff_phases,
(
"Failure verdicts that must hold the next reconnect attempt\n"
"(Connection::onDisconnectedInternal exponential backoff)."
),
),
(
"isObservationFacadePhase",
observation_facade_phases,
(
"Phases published through the MtProxySocketObservation facade\n"
"(mtProxySocketObservationIsHighRiskPhase): failures that must record\n"
"an endpoint failure at close, plus the data-path success markers\n"
"routed the same way."
),
),
(
"isLocalSchedulerTimeout",
local_scheduler_timeout_phases,
(
"Local scheduler/gate timeouts: the wait was produced by our own\n"
"pre-TCP machinery (admission queue, endpoint cooldown, gates), not\n"
"by the network. MtProxyEndpointRecorder::recordFailure skips these so a local\n"
"wake-up never counts as an endpoint failure. Deliberately excludes\n"
"mtproxy_probe_wait_timeout and includes background_handshake_aborted."
),
),
)
def contract_constant_values() -> set[str]:
contract = CONTRACT_H.read_text(encoding="utf-8")
return {
value
for _, value in re.findall(
r'constexpr const char \*([A-Za-z0-9_]+)\s*=\s*"([a-z0-9_]+)"', contract
)
}
def ordered(names: set[str]) -> list[str]:
contract_order = [phase.name for phase in PHASES] + list(FACADE_ONLY_PHASES)
return [name for name in contract_order if name in names]
def render() -> str:
constants = contract_constant_values()
lines = [
"// AUTOGENERATED by Tools/generate_mtproxy_phase_classification.py - DO NOT EDIT.",
"// Source of truth: Tools/mtproxy_phase_contract.py. After editing the contract",
"// run the generator; check_mtproxy_all.py fails while this file is stale.",
"#ifndef MTPROXYPHASECLASSIFICATION_H",
"#define MTPROXYPHASECLASSIFICATION_H",
"",
"#include <string.h>",
"",
"namespace MtProxyPhase {",
]
for function_name, phase_source, doc in FUNCTIONS:
phases = ordered(phase_source())
if not phases:
raise SystemExit(f"{function_name}: contract returned no phases")
missing = [name for name in phases if name not in constants]
if missing:
raise SystemExit(
f"{function_name}: phases missing a constexpr constant in "
f"MtProxyPhaseContract.h: {', '.join(missing)}"
)
lines.append("")
for doc_line in doc.splitlines():
lines.append(f"// {doc_line}")
lines.append(f"inline bool {function_name}(const char *phase) {{")
lines.append(" if (phase == nullptr) {")
lines.append(" return false;")
lines.append(" }")
for index, name in enumerate(phases):
prefix = " return " if index == 0 else " || "
suffix = ";" if index == len(phases) - 1 else ""
lines.append(f'{prefix}strcmp(phase, "{name}") == 0{suffix}')
lines.append("}")
lines.extend(["}", "", "#endif", ""])
return "\n".join(lines)
def java_bool_function(name: str, phases: list[str], doc: str) -> list[str]:
lines = [""]
for doc_line in doc.splitlines():
lines.append(f" // {doc_line}")
lines.append(" // Pass a ProxyCheckDiagnostics.normalize()d phase.")
lines.append(f" public static boolean {name}(String phase) {{")
lines.append(" if (phase == null) {")
lines.append(" return false;")
lines.append(" }")
lines.append(" switch (phase) {")
for phase in phases:
lines.append(f' case "{phase}":')
lines.append(" return true;")
lines.append(" default:")
lines.append(" return false;")
lines.append(" }")
lines.append(" }")
return lines
def java_int_function(name: str, groups: list[tuple[str, list[str]]], default: str, doc: str) -> list[str]:
lines = [""]
for doc_line in doc.splitlines():
lines.append(f" // {doc_line}")
lines.append(" // Pass a ProxyCheckDiagnostics.normalize()d phase.")
lines.append(f" public static int {name}(String phase) {{")
lines.append(" if (phase == null) {")
lines.append(f" return {default};")
lines.append(" }")
lines.append(" switch (phase) {")
for constant, phases in groups:
if not phases:
continue
for phase in phases:
lines.append(f' case "{phase}":')
lines.append(f" return {constant};")
lines.append(" default:")
lines.append(f" return {default};")
lines.append(" }")
lines.append(" }")
return lines
def render_java_phase_table() -> list[str]:
java_phases = [phase for phase in PHASES if phase.java]
ordered_names = [phase.name for phase in java_phases]
kind_groups = [
("KIND_NEUTRAL", [p.name for p in java_phases if p.kind == PHASE_NEUTRAL]),
("KIND_LIVE", [p.name for p in java_phases if p.kind == PHASE_LIVE]),
("KIND_SUCCESS", [p.name for p in java_phases if p.kind == PHASE_SUCCESS]),
("KIND_FAILURE", [p.name for p in java_phases if p.kind == PHASE_FAILURE]),
]
scope_groups = [
("SCOPE_NONE", [p.name for p in java_phases if java_key_scope_for(p) == "none"]),
("SCOPE_EXACT", [p.name for p in java_phases if java_key_scope_for(p) == "exact"]),
("SCOPE_NETWORK", [p.name for p in java_phases if java_key_scope_for(p) == "network"]),
]
lines = [
"",
" public static final int KIND_NEUTRAL = 0;",
" public static final int KIND_LIVE = 1;",
" public static final int KIND_SUCCESS = 2;",
" public static final int KIND_FAILURE = 3;",
"",
" public static final int SCOPE_NONE = 0;",
" public static final int SCOPE_EXACT = 1;",
" public static final int SCOPE_NETWORK = 2;",
]
lines += java_bool_function(
"isKnownJavaPhase",
ordered_names,
(
"Every phase the Java layer models. Unknown phases fall back to\n"
"ProxyPhasePolicy's conservative default (backoff-eligible exact\n"
"failure), matching the historical switch default."
),
)
lines += java_int_function(
"phaseKind",
kind_groups,
"KIND_FAILURE",
"Phase kind from the contract (kind= field).",
)
lines += java_int_function(
"phaseKeyScope",
scope_groups,
"SCOPE_EXACT",
(
"Java-side endpoint key scope: endpoint_key from the contract with\n"
"explicit java_key_scope overrides (waiting_tcp, unknown_fail)."
),
)
lines += java_bool_function(
"javaBackoff",
ordered(java_backoff_phases()),
(
"Java scheduler ACTION_BACKOFF eligibility: the rotation flag with\n"
"explicit java_backoff overrides (unknown_fail paces retries\n"
"without rotating)."
),
)
lines += java_bool_function(
"javaRotate",
ordered(java_rotation_phases()),
"Rotation-eligible failures (rotation=True in the contract).",
)
return lines
def render_java() -> str:
constants = contract_constant_values()
lines = [
"// AUTOGENERATED by Tools/generate_mtproxy_phase_classification.py - DO NOT EDIT.",
"// Source of truth: Tools/mtproxy_phase_contract.py. After editing the contract",
"// run the generator; check_mtproxy_all.py fails while this file is stale.",
"package org.telegram.messenger;",
"",
"public final class ProxyPhaseClassification {",
"",
" private ProxyPhaseClassification() {",
" }",
]
for function_name, phase_source, doc in FUNCTIONS:
phases = ordered(phase_source())
missing = [name for name in phases if name not in constants]
if missing:
raise SystemExit(
f"{function_name}: phases missing a constexpr constant in "
f"MtProxyPhaseContract.h: {', '.join(missing)}"
)
lines.append("")
for doc_line in doc.splitlines():
lines.append(f" // {doc_line}")
lines.append(" // Pass a ProxyCheckDiagnostics.normalize()d phase.")
lines.append(f" public static boolean {function_name}(String phase) {{")
lines.append(" if (phase == null) {")
lines.append(" return false;")
lines.append(" }")
lines.append(" switch (phase) {")
for name in phases:
lines.append(f' case "{name}":')
lines.append(" return true;")
lines.append(" default:")
lines.append(" return false;")
lines.append(" }")
lines.append(" }")
lines += render_java_phase_table()
lines.extend(["}", ""])
return "\n".join(lines)
def main() -> int:
rendered_h = render()
rendered_java = render_java()
if "--check" in sys.argv[1:]:
stale = []
for path, rendered in ((OUTPUT_H, rendered_h), (OUTPUT_JAVA, rendered_java)):
current = path.read_text(encoding="utf-8") if path.exists() else ""
if current != rendered:
stale.append(path.name)
if stale:
print(
f"stale generated classification: {', '.join(stale)} - rerun "
"Tools/generate_mtproxy_phase_classification.py",
file=sys.stderr,
)
return 1
print("MtProxy phase classification (native + Java) is in sync.")
return 0
OUTPUT_H.write_text(rendered_h, encoding="utf-8", newline="\n")
print(f"wrote {OUTPUT_H}")
OUTPUT_JAVA.write_text(rendered_java, encoding="utf-8", newline="\n")
print(f"wrote {OUTPUT_JAVA}")
return 0
if __name__ == "__main__":
raise SystemExit(main())