Skip to content

Commit 5e19fc7

Browse files
committed
test with this?
1 parent 4bdec54 commit 5e19fc7

File tree

2 files changed

+73
-126
lines changed

2 files changed

+73
-126
lines changed
-2.7 KB
Binary file not shown.

src/updater/update_checker.py

Lines changed: 73 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import hashlib
77
import textwrap
88
import tempfile
9+
from packaging.version import Version as SemVer
910
import requests
1011
from PyQt6.QtCore import QObject, pyqtSignal
1112
from tuf.ngclient.updater import Updater # TUF verification
@@ -49,6 +50,8 @@ def __init__(self, repo_owner: str, repo_name: str, version: str, app_install_di
4950
fetcher=self._fetcher,
5051
)
5152

53+
self.bundle_extensions = (".tar.gz", ".zip")
54+
5255
@staticmethod
5356
def _extract_archive(archive_path: str, dest_dir: str) -> None:
5457
"""Extract .tar.* or .zip archives into dest_dir."""
@@ -85,22 +88,33 @@ def _platform_prefix(self) -> str:
8588
else:
8689
return "telemetry-linux-"
8790

88-
@staticmethod
89-
def _bundle_name_for(version: str) -> str:
91+
def _bundle_base(self, version: str) -> str:
92+
if sys.platform.startswith("win"):
93+
app = "telemetry-windows"
94+
elif sys.platform == "darwin":
95+
app = "telemetry-macos"
96+
else:
97+
app = "telemetry-linux"
98+
return f"{app}-{version}"
99+
100+
def _resolve_bundle(self, updater: Updater, version: str):
101+
base = self._bundle_base(version)
102+
for ext in self.bundle_extensions:
103+
name = base + ext
104+
ti = updater.get_targetinfo(name)
105+
if ti is not None:
106+
return name, ti
107+
return None, None
108+
109+
def _bundle_name_for(self, version: str) -> str:
90110
"""
91111
Compute the TUF target file name (tar.gz bundle) for the given
92112
version. This matches scripts/build_tuf_repo.py where we call
93113
repo.add_bundle with app_name per platform:
94114
telemetry-windows | telemetry-macos | telemetry-linux
95115
-> bundle names: telemetry-<platform>-<version>.tar.gz
96116
"""
97-
if sys.platform.startswith("win"):
98-
app = "telemetry-windows"
99-
elif sys.platform == "darwin":
100-
app = "telemetry-macos"
101-
else:
102-
app = "telemetry-linux"
103-
return f"{app}-{version}.tar.gz"
117+
return self._bundle_base(version) + self.bundle_extensions[0]
104118

105119
# ---------- helpers ----------
106120
def _latest_version_from_github(self) -> str | None:
@@ -127,140 +141,73 @@ def _running_binary_path(self) -> str:
127141

128142
# ---------- public API ----------
129143
def check_for_updates(self) -> bool:
130-
"""
131-
Returns True if a newer version than self.version is available.
132-
Discovery via GitHub API; download/verify via TUF.
133-
"""
134-
# Dev mode: skip
144+
"""Return True if a newer version is available and staged in TUF."""
135145
if not getattr(sys, "frozen", False):
136146
return False
137147

138148
try:
139-
latest = self._latest_version_from_github()
140-
if not latest or latest == self.version:
141-
return False # up-to-date or unknown
142-
143-
# Verify the bundle for the latest version exists in TUF metadata
144-
self.updater.refresh()
145-
bundle_name = self._bundle_name_for(latest)
146-
ti = self.updater.get_targetinfo(bundle_name)
147-
if ti is None:
148-
self.update_error.emit(
149-
f"TUF metadata does not contain bundle '{bundle_name}'. "
150-
f"Make sure your release assets include TUF metadata + {bundle_name}."
151-
)
149+
versions = self.list_available_versions(limit=10)
150+
if not versions:
152151
return False
153152

154-
self.update_available.emit(latest)
155-
return True
153+
current = SemVer(self.version)
154+
for candidate in versions:
155+
try:
156+
cand_ver = SemVer(candidate)
157+
except Exception:
158+
continue
159+
if cand_ver <= current:
160+
continue
161+
if self._version_has_bundle(candidate):
162+
self.update_available.emit(candidate)
163+
return True
164+
return False
156165
except Exception as e:
157166
self.update_error.emit(str(e))
158167
return False
159168

169+
def _version_has_bundle(self, version: str) -> bool:
170+
try:
171+
base = f"https://github.com/{self.repo_owner}/{self.repo_name}/releases/download/v{version}"
172+
updater = Updater(
173+
self.metadata_dir,
174+
metadata_base_url=base + "/",
175+
target_base_url=base + "/",
176+
fetcher=self._fetcher,
177+
)
178+
updater.refresh()
179+
name, ti = self._resolve_bundle(updater, version)
180+
return ti is not None
181+
except Exception:
182+
return False
183+
160184
def download_and_apply_update(self) -> bool:
161-
"""
162-
Downloads {target_name} using TUF (verified), then atomically swaps it in.
163-
Handles Windows by spawning a small .bat to replace after exit.
164-
"""
165-
# Dev mode: skip
166185
if not getattr(sys, "frozen", False):
167186
self.update_error.emit("Updater only runs in a packaged build.")
168187
return False
169188

170-
try:
171-
latest = self._latest_version_from_github() or self.version
172-
bundle_name = self._bundle_name_for(latest)
173-
ti = self.updater.get_targetinfo(bundle_name)
174-
if ti is None:
175-
self.update_error.emit(f"Bundle '{bundle_name}' not found in TUF metadata.")
176-
return False
177-
178-
# Download the tar.gz bundle
179-
bundle_path = os.path.join(self.download_dir, bundle_name)
180-
# Use our progress fetcher to emit per-byte progress
181-
def _emit(received: int, total: int | None, pct: int | None):
182-
if pct is not None:
183-
try:
184-
self.update_progress.emit(int(pct))
185-
except Exception:
186-
pass
187-
self._fetcher.set_callback(_emit)
188-
self.updater.download_target(ti, filepath=bundle_path)
189-
self._fetcher.set_callback(None)
190-
191-
# Extract bundle and locate the binary inside
192-
extract_dir = tempfile.mkdtemp(prefix="tuf_bundle_")
193-
try:
194-
self._extract_archive(bundle_path, extract_dir)
195-
except Exception as e:
196-
self.update_error.emit(f"Failed to extract bundle: {e}")
197-
return False
198-
199-
# Find expected binary inside extracted contents
200-
binary_name = self.target_name
201-
candidate = None
202-
bundle_root = None
203-
for root, _dirs, files in os.walk(extract_dir):
204-
if candidate is None and binary_name in files:
205-
candidate = os.path.join(root, binary_name)
206-
bundle_root = root
207-
if candidate and bundle_root:
208-
break
209-
if not candidate or not bundle_root:
210-
self.update_error.emit(f"Bundle did not contain expected binary '{binary_name}'.")
211-
return False
189+
versions = self.list_available_versions(limit=10)
190+
if not versions:
191+
self.update_error.emit("No releases found.")
192+
return False
212193

213-
staged_root = os.path.join(self.download_dir, "staged_bundle")
194+
current = SemVer(self.version)
195+
target = None
196+
for candidate in versions:
214197
try:
215-
if os.path.exists(staged_root):
216-
shutil.rmtree(staged_root, ignore_errors=True)
217-
shutil.copytree(bundle_root, staged_root, dirs_exist_ok=True)
218-
except Exception as e:
219-
self.update_error.emit(f"Failed staging new bundle: {e}")
220-
return False
221-
222-
new_exe_path = os.path.join(staged_root, binary_name)
223-
224-
old_exe = self._running_binary_path()
225-
if not old_exe:
226-
self.update_error.emit("No runnable binary path detected.")
227-
return False
228-
229-
if os.name == "nt":
230-
bat_path = os.path.join(self.download_dir, "apply_update.bat")
231-
app_dir = os.path.dirname(old_exe)
232-
script = textwrap.dedent(f"""
233-
@echo off
234-
set NEW_DIR="{staged_root}"
235-
set OLD_EXE="{old_exe}"
236-
set APP_DIR="{app_dir}"
237-
:wait
238-
ping 127.0.0.1 -n 2 >nul
239-
tasklist /FI "PID eq {os.getpid()}" | findstr /I "{os.getpid()}" >nul && goto wait
240-
robocopy %NEW_DIR% %APP_DIR% /E /COPY:DAT /R:2 /W:1 /NFL /NDL /NJH /NJS >nul
241-
if errorlevel 8 echo Robocopy returned %errorlevel%
242-
start "" %OLD_EXE%
243-
""")
244-
with open(bat_path, "w", encoding="utf-8") as f:
245-
f.write(script)
246-
env = os.environ.copy()
247-
subprocess.Popen(["cmd", "/c", bat_path], creationflags=0x08000000, env=env)
248-
sys.exit(0)
249-
else:
250-
backup = old_exe + ".bak"
251-
try:
252-
os.replace(old_exe, backup)
253-
except Exception:
254-
pass
255-
os.replace(new_exe_path, old_exe)
256-
env = os.environ.copy()
257-
subprocess.Popen([old_exe] + sys.argv[1:], cwd=os.path.dirname(old_exe), env=env)
258-
sys.exit(0)
259-
260-
except Exception as e:
261-
self.update_error.emit(str(e))
198+
cand_ver = SemVer(candidate)
199+
except Exception:
200+
continue
201+
if cand_ver > current and self._version_has_bundle(candidate):
202+
target = candidate
203+
break
204+
205+
if not target:
206+
self.update_error.emit("Already running latest version.")
262207
return False
263208

209+
return self.download_and_apply_version(target)
210+
264211
# ---------- multi-version support ----------
265212
def list_available_versions(self, limit: int = 15) -> list[str]:
266213
"""
@@ -282,7 +229,7 @@ def list_available_versions(self, limit: int = 15) -> list[str]:
282229
tag = tag[1:]
283230
# Ensure matching asset exists
284231
assets = rel.get('assets') or []
285-
found = any((a.get('name') or '').startswith(pref) and (a.get('name') or '').endswith('.tar.gz') for a in assets)
232+
found = any((a.get('name') or '').startswith(pref) and (a.get('name') or '').endswith(tuple(self.bundle_extensions)) for a in assets)
286233
if found and tag:
287234
versions.append(tag)
288235
# Deduplicate while preserving order
@@ -416,4 +363,4 @@ def _extract_archive(self, archive_path: str, dest_dir: str) -> None:
416363
else:
417364
import tarfile
418365
with tarfile.open(archive_path, "r:*") as tf:
419-
tf.extractall(path=dest_dir)
366+
tf.extractall(path=dest_dir)

0 commit comments

Comments
 (0)