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
324 lines
12 KiB
Python
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())
|