This commit is contained in:
Your Name
2026-08-28 18:24:37 +08:00
parent 43ad07208f
commit ed48f8be31
383 changed files with 8673 additions and 2222 deletions
+447
View File
@@ -0,0 +1,447 @@
"""Read-only-ish timing probe for the desktop update commit sequence.
The probe imports production code and replaces only its external download/apply
edges in memory. It does not modify production sources or existing tests.
"""
from __future__ import annotations
import gc
import json
import os
import subprocess
import sys
import tempfile
import threading
import time
import weakref
from pathlib import Path
from types import SimpleNamespace
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QCoreApplication, QEvent, QObject, QThreadPool, QTimer
from PySide6.QtWidgets import QApplication
from doctor_workstation.services import app_update as update_service
from doctor_workstation.services.app_update import (
PACKAGE_TYPE_INNO_SETUP,
UpdateOffer,
UpdatePackage,
)
from doctor_workstation.ui.dialogs import app_update as update_ui
def _record(events: list[dict[str, Any]], name: str, started: float) -> None:
events.append(
{
"event": name,
"elapsed_ms": round((time.perf_counter() - started) * 1000, 3),
"thread_id": threading.get_ident(),
}
)
def exercise_session(iterations: int = 25) -> dict[str, Any]:
app = QApplication.instance() or QApplication([])
main_thread = threading.get_ident()
original_edges = {
"is_frozen_install": update_ui.is_frozen_install,
"frozen_install_root": update_ui.frozen_install_root,
"download_package": update_ui.download_package,
"apply_downloaded_update": update_ui.apply_downloaded_update,
}
failures: list[dict[str, Any]] = []
samples: list[list[dict[str, Any]]] = []
collected_signals = 0
try:
with tempfile.TemporaryDirectory(prefix="zyt-update-commit-") as raw_tmp:
temp_root = Path(raw_tmp)
install_root = temp_root / "installed"
install_root.mkdir()
(install_root / "DoctorWorkstation.exe").write_bytes(b"MZ")
update_ui.is_frozen_install = lambda: True
update_ui.frozen_install_root = lambda: install_root
for index in range(iterations):
run_root = temp_root / f"run-{index}"
run_root.mkdir()
events: list[dict[str, Any]] = []
started = time.perf_counter()
def fake_download(
_url: str,
destination: Path,
*,
_events: list[dict[str, Any]] = events,
_started: float = started,
**kwargs: Any,
) -> Path:
_record(_events, "download_enter", _started)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(b"MZ" + b"probe")
progress = kwargs.get("progress")
if callable(progress):
progress(7, 7)
_record(_events, "download_return", _started)
return destination
def fake_apply(
_payload: Path,
*,
package_type: str,
_events: list[dict[str, Any]] = events,
_started: float = started,
) -> None:
assert package_type == PACKAGE_TYPE_INNO_SETUP
_record(_events, "apply_enter", _started)
_record(_events, "apply_return", _started)
update_ui.download_package = fake_download
update_ui.apply_downloaded_update = fake_apply
host = QObject()
host.config = SimpleNamespace( # type: ignore[attr-defined]
config_dir=run_root,
verify_ssl=True,
)
def request_quit(
_events: list[dict[str, Any]] = events,
_started: float = started,
) -> None:
_record(_events, "request_quit", _started)
host.request_quit = request_quit # type: ignore[attr-defined]
session = update_ui.AppUpdateSession(host)
offer = UpdateOffer(
has_update=True,
force=True,
enabled=True,
current_version="1.0.0",
latest_version=f"1.0.{index + 1}",
min_version="",
title="probe",
notes="probe",
platform="windows",
arch="x64",
package=UpdatePackage(
url="https://example.invalid/DoctorWorkstation-Setup.exe",
sha256="a" * 64,
size=7,
filename="DoctorWorkstation-Setup.exe",
type=PACKAGE_TYPE_INNO_SETUP,
),
can_install=True,
)
dialog = update_ui.AppUpdateDialog(offer)
session.dialog = dialog
original_finish = session._finish_install
original_finished = session._on_install_finished
def finish_probe(
*args: Any,
_events: list[dict[str, Any]] = events,
_started: float = started,
_original: Any = original_finish,
**kwargs: Any,
) -> None:
_record(_events, "result_slot_enter", _started)
_original(*args, **kwargs)
_record(_events, "result_slot_return", _started)
def finished_probe(
*args: Any,
_events: list[dict[str, Any]] = events,
_started: float = started,
_original: Any = original_finished,
**kwargs: Any,
) -> None:
_record(_events, "finished_slot_enter", _started)
_original(*args, **kwargs)
_record(_events, "finished_slot_return", _started)
session._finish_install = finish_probe # type: ignore[method-assign]
session._on_install_finished = finished_probe # type: ignore[method-assign]
session._start_install(dialog, offer)
signal_ref = weakref.ref(session._active_install_signals)
deadline = time.perf_counter() + 3.0
while time.perf_counter() < deadline:
app.processEvents()
if any(item["event"] == "request_quit" for item in events):
break
time.sleep(0.001)
QThreadPool.globalInstance().waitForDone(3000)
app.processEvents()
names = [item["event"] for item in events]
expected = [
"download_enter",
"download_return",
"result_slot_enter",
"apply_enter",
"apply_return",
"result_slot_return",
"finished_slot_enter",
"request_quit",
"finished_slot_return",
]
slot_threads = {
item["thread_id"]
for item in events
if item["event"] in {"result_slot_enter", "finished_slot_enter", "request_quit"}
}
if names != expected or slot_threads != {main_thread}:
failures.append(
{
"iteration": index,
"events": events,
"main_thread_id": main_thread,
}
)
if index < 3:
samples.append(events)
session.deleteLater()
dialog.deleteLater()
del session, dialog, host
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
app.processEvents()
gc.collect()
app.processEvents()
if signal_ref() is None:
collected_signals += 1
finally:
update_ui.is_frozen_install = original_edges["is_frozen_install"]
update_ui.frozen_install_root = original_edges["frozen_install_root"]
update_ui.download_package = original_edges["download_package"]
update_ui.apply_downloaded_update = original_edges["apply_downloaded_update"]
return {
"iterations": iterations,
"failures": failures,
"signals_collected_after_iteration": collected_signals,
"main_thread_id": main_thread,
"samples": samples,
}
def exercise_real_popen() -> dict[str, Any]:
if sys.platform != "win32":
return {"skipped": f"requires win32, got {sys.platform}"}
with tempfile.TemporaryDirectory(prefix="zyt-update-helper-") as raw_tmp:
temp_root = Path(raw_tmp)
script = temp_root / "probe_helper.ps1"
script.write_text(
"\n".join(
[
"param(",
" [int]$TargetPid, [string]$Installer, [string]$RestartExe,",
" [string]$HelperLogFile, [string]$InstallerLogFile",
")",
"Set-Content -LiteralPath $HelperLogFile -Value 'child-started'",
"Start-Sleep -Milliseconds 1200",
"Set-Content -LiteralPath $HelperLogFile -Value 'child-complete'",
]
),
encoding="utf-8-sig",
)
installer = temp_root / "Setup.exe"
installer.write_bytes(b"MZ")
restart = temp_root / "DoctorWorkstation.exe"
restart.write_bytes(b"MZ")
helper_log = temp_root / "helper.log"
fixed_log = temp_root / "fixed.log"
installer_log = temp_root / "installer.log"
original_script = script.read_text(encoding="utf-8-sig")
fixed_log_literal = str(fixed_log).replace("'", "''")
script.write_text(
original_script.replace(
"Set-Content -LiteralPath $HelperLogFile -Value 'child-started'",
"Set-Content -LiteralPath '"
+ fixed_log_literal
+ "' -Value (\"helper=<{0}> args=<{1}>\" -f $HelperLogFile, ($args -join '|'))\n"
+ "Set-Content -LiteralPath $HelperLogFile -Value 'child-started'",
),
encoding="utf-8-sig",
)
captured: list[subprocess.Popen[Any]] = []
captured_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
real_popen = update_service.subprocess.Popen
def capture_popen(*args: Any, **kwargs: Any) -> subprocess.Popen[Any]:
captured_calls.append((args, kwargs.copy()))
process = real_popen(*args, **kwargs)
captured.append(process)
return process
update_service.subprocess.Popen = capture_popen
try:
started = time.perf_counter()
update_service._spawn_inno_setup_applier(
script,
installer=installer,
restart_exe=restart,
helper_log_file=helper_log,
installer_log_file=installer_log,
)
returned_ms = round((time.perf_counter() - started) * 1000, 3)
finally:
update_service.subprocess.Popen = real_popen
deadline = time.perf_counter() + 2.5
child_log = ""
while time.perf_counter() < deadline:
if helper_log.exists():
child_log = helper_log.read_text(encoding="utf-8").strip()
if child_log == "child-complete":
break
time.sleep(0.05)
return_code = captured[0].poll() if captured else None
if captured and return_code is None:
return_code = captured[0].wait(timeout=2.0)
matrix: dict[str, Any] = {}
if captured_calls:
command = list(captured_calls[0][0][0])
flag_cases = {
"zero": 0,
"detached": getattr(subprocess, "DETACHED_PROCESS", 0),
"new_process_group": getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
"no_window": getattr(subprocess, "CREATE_NO_WINDOW", 0),
"detached_new_group": getattr(subprocess, "DETACHED_PROCESS", 0)
| getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
"detached_no_window": getattr(subprocess, "DETACHED_PROCESS", 0)
| getattr(subprocess, "CREATE_NO_WINDOW", 0),
"new_group_no_window": getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
| getattr(subprocess, "CREATE_NO_WINDOW", 0),
"production_all": getattr(subprocess, "DETACHED_PROCESS", 0)
| getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
| getattr(subprocess, "CREATE_NO_WINDOW", 0),
}
running: dict[str, tuple[subprocess.Popen[Any], Path]] = {}
helper_parameter = command.index("-HelperLogFile") + 1
for name, flags in flag_cases.items():
case_command = command.copy()
case_log = temp_root / f"matrix-{name}.log"
case_command[helper_parameter] = str(case_log)
process = real_popen(
case_command,
close_fds=True,
creationflags=flags,
cwd=str(temp_root),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
running[name] = (process, case_log)
production_flags = flag_cases["production_all"]
detached_flags = flag_cases["detached"]
extra_cases = {
"production_close_false": (production_flags, False, False),
"production_all_devnull": (production_flags, True, True),
"detached_close_false": (detached_flags, False, False),
"detached_all_devnull": (detached_flags, True, True),
}
for name, (flags, close_fds, all_devnull) in extra_cases.items():
case_command = command.copy()
case_log = temp_root / f"matrix-{name}.log"
case_command[helper_parameter] = str(case_log)
stream_kwargs = (
{
"stdin": subprocess.DEVNULL,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
}
if all_devnull
else {}
)
process = real_popen(
case_command,
close_fds=close_fds,
creationflags=flags,
cwd=str(temp_root),
**stream_kwargs,
)
flag_cases[name] = flags
running[name] = (process, case_log)
matrix_deadline = time.perf_counter() + 3.0
while time.perf_counter() < matrix_deadline:
if all(case_log.exists() for _, case_log in running.values()):
break
time.sleep(0.05)
for name, (process, case_log) in running.items():
matrix[name] = {
"flags": flag_cases[name],
"log_created": case_log.exists(),
"return_code": process.poll(),
}
control_return_code = None
control_stdout = ""
control_stderr = ""
if not fixed_log.exists() and captured_calls:
call_args, call_kwargs = captured_calls[0]
call_kwargs.update(
creationflags=0,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
control = real_popen(*call_args, **call_kwargs)
control_stdout, control_stderr = control.communicate(timeout=5.0)
control_return_code = control.returncode
return {
"command": captured_calls[0][0][0] if captured_calls else [],
"spawn_returned_ms": returned_ms,
"child_completed": child_log == "child-complete",
"child_log": child_log,
"fixed_log": fixed_log.read_text(encoding="utf-8").strip()
if fixed_log.exists()
else "",
"child_return_code": return_code,
"flag_matrix": matrix,
"control_return_code": control_return_code,
"control_stdout": control_stdout,
"control_stderr": control_stderr,
}
def exercise_controller_quit() -> dict[str, Any]:
"""Run the production request_quit method against a real Qt event loop."""
from doctor_workstation.app import ApplicationController
app = QApplication.instance() or QApplication([])
events: list[str] = []
holder = SimpleNamespace(application=app, _shutting_down=False)
app.aboutToQuit.connect(lambda: events.append("aboutToQuit"))
QTimer.singleShot(
0,
lambda: (
events.append("request_quit_enter"),
ApplicationController.request_quit(holder),
events.append("request_quit_return"),
),
)
watchdog = QTimer()
watchdog.setSingleShot(True)
watchdog.timeout.connect(lambda: (events.append("watchdog"), app.quit()))
watchdog.start(1000)
started = time.perf_counter()
return_code = app.exec()
return {
"events": events,
"return_code": return_code,
"returned_ms": round((time.perf_counter() - started) * 1000, 3),
}
if __name__ == "__main__":
iteration_count = int(sys.argv[1]) if len(sys.argv) > 1 else 25
output = {
"session": exercise_session(iteration_count),
"real_popen": exercise_real_popen(),
"controller_quit": exercise_controller_quit(),
}
print(json.dumps(output, ensure_ascii=False, indent=2))