Optimize server startup speed, client update caching, and HTTP 304 conditional downloads
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m28s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m28s
This commit is contained in:
+47
-6
@@ -1497,11 +1497,17 @@ def check_client_update(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/client/update/download")
|
@app.get("/api/client/update/download")
|
||||||
def download_client_update_package(os: Optional[str] = "windows", os_name: Optional[str] = None):
|
def download_client_update_package(
|
||||||
|
request: Request,
|
||||||
|
os: Optional[str] = "windows",
|
||||||
|
os_name: Optional[str] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Streams the latest client binary or installer package for the specified OS.
|
Streams the latest client binary or installer package for the specified OS.
|
||||||
|
Supports HTTP ETag and conditional 304 Not Modified requests to minimize network bandwidth.
|
||||||
"""
|
"""
|
||||||
import os as _os
|
import os as _os
|
||||||
|
import email.utils
|
||||||
target_os = os_name or os or "windows"
|
target_os = os_name or os or "windows"
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
pkg_tuple = mgr.get_client_package_path(os_type=target_os)
|
pkg_tuple = mgr.get_client_package_path(os_type=target_os)
|
||||||
@@ -1512,14 +1518,50 @@ def download_client_update_package(os: Optional[str] = "windows", os_name: Optio
|
|||||||
if not _os.path.exists(file_path):
|
if not _os.path.exists(file_path):
|
||||||
raise HTTPException(status_code=404, detail="Client package file missing on server disk.")
|
raise HTTPException(status_code=404, detail="Client package file missing on server disk.")
|
||||||
|
|
||||||
|
etag = f'"{sha256}"'
|
||||||
|
if request is not None:
|
||||||
|
if_none_match = request.headers.get("if-none-match")
|
||||||
|
if if_none_match and (if_none_match == etag or if_none_match.strip('"') == sha256):
|
||||||
|
return Response(
|
||||||
|
status_code=304,
|
||||||
|
headers={
|
||||||
|
"ETag": etag,
|
||||||
|
"Cache-Control": "public, max-age=300, must-revalidate",
|
||||||
|
"X-Checksum-SHA256": sha256
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mtime = _os.path.getmtime(file_path)
|
||||||
|
last_modified = email.utils.formatdate(mtime, usegmt=True)
|
||||||
|
except Exception:
|
||||||
|
last_modified = None
|
||||||
|
|
||||||
|
if_modified_since = request.headers.get("if-modified-since")
|
||||||
|
if if_modified_since and last_modified and if_modified_since == last_modified:
|
||||||
|
return Response(
|
||||||
|
status_code=304,
|
||||||
|
headers={
|
||||||
|
"ETag": etag,
|
||||||
|
"Cache-Control": "public, max-age=300, must-revalidate",
|
||||||
|
"X-Checksum-SHA256": sha256
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"ETag": etag,
|
||||||
|
"Cache-Control": "public, max-age=300, must-revalidate",
|
||||||
|
"X-Checksum-SHA256": sha256,
|
||||||
|
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||||
|
}
|
||||||
|
if last_modified:
|
||||||
|
headers["Last-Modified"] = last_modified
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=file_path,
|
path=file_path,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
headers={
|
headers=headers
|
||||||
"X-Checksum-SHA256": sha256,
|
|
||||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2360,7 +2402,6 @@ def main():
|
|||||||
csr_path = os.path.join(cert_dir, "ca.csr")
|
csr_path = os.path.join(cert_dir, "ca.csr")
|
||||||
if not os.path.exists(csr_path):
|
if not os.path.exists(csr_path):
|
||||||
enrollment.generate_ca_csr(cert_dir=cert_dir)
|
enrollment.generate_ca_csr(cert_dir=cert_dir)
|
||||||
export_license_request_lic(config)
|
|
||||||
|
|
||||||
srv_cert, srv_key, srv_pem, srv_key_pem = enrollment.generate_server_cert_if_needed(
|
srv_cert, srv_key, srv_pem, srv_key_pem = enrollment.generate_server_cert_if_needed(
|
||||||
ca_cert, ca_key,
|
ca_cert, ca_key,
|
||||||
|
|||||||
+37
-11
@@ -76,22 +76,48 @@ class ClientUpdater:
|
|||||||
def download_package(self, download_path_rel: str, expected_sha256: str, target_temp: str, timeout: int = 30) -> bool:
|
def download_package(self, download_path_rel: str, expected_sha256: str, target_temp: str, timeout: int = 30) -> bool:
|
||||||
"""
|
"""
|
||||||
Downloads the client binary package from the server and validates its SHA-256 checksum.
|
Downloads the client binary package from the server and validates its SHA-256 checksum.
|
||||||
|
Uses local binary hash checking and ETag conditional headers to avoid redundant downloads.
|
||||||
"""
|
"""
|
||||||
|
# 1. If running binary already matches target SHA-256, skip downloading
|
||||||
|
target_exec = sys.executable
|
||||||
|
if expected_sha256 and os.path.exists(target_exec):
|
||||||
|
try:
|
||||||
|
local_sha = self.calculate_sha256(target_exec)
|
||||||
|
if local_sha and local_sha == expected_sha256.lower():
|
||||||
|
print(f"[*] Local binary already matches target checksum ({local_sha[:12]}...). Skipping redundant download.")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
base_url = self.get_server_base_url()
|
base_url = self.get_server_base_url()
|
||||||
download_url = f"{base_url.rstrip('/')}/{download_path_rel.lstrip('/')}"
|
download_url = f"{base_url.rstrip('/')}/{download_path_rel.lstrip('/')}"
|
||||||
|
|
||||||
print(f"[*] Downloading client update package from {download_url}...")
|
print(f"[*] Downloading client update package from {download_url}...")
|
||||||
req = urllib.request.Request(
|
headers = {"User-Agent": f"LOGAR-Client/{self.current_version} ({self.os_type})"}
|
||||||
download_url,
|
if expected_sha256:
|
||||||
headers={"User-Agent": f"LOGAR-Client/{self.current_version} ({self.os_type})"}
|
headers["If-None-Match"] = f'"{expected_sha256}"'
|
||||||
)
|
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
req = urllib.request.Request(download_url, headers=headers)
|
||||||
if resp.status != 200:
|
try:
|
||||||
print(f"[!] Server returned HTTP {resp.status} during update download.")
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
return False
|
if resp.status == 304:
|
||||||
with open(target_temp, "wb") as f:
|
print("[+] Server returned 304 Not Modified. Local binary package is current.")
|
||||||
while chunk := resp.read(65536):
|
return True
|
||||||
f.write(chunk)
|
if resp.status != 200:
|
||||||
|
print(f"[!] Server returned HTTP {resp.status} during update download.")
|
||||||
|
return False
|
||||||
|
with open(target_temp, "wb") as f:
|
||||||
|
while chunk := resp.read(65536):
|
||||||
|
f.write(chunk)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 304:
|
||||||
|
print("[+] Server returned 304 Not Modified. Local binary package is current.")
|
||||||
|
return True
|
||||||
|
print(f"[!] HTTP error during update download: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Error downloading update package: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Verify SHA-256
|
# Verify SHA-256
|
||||||
actual_sha256 = self.calculate_sha256(target_temp)
|
actual_sha256 = self.calculate_sha256(target_temp)
|
||||||
|
|||||||
+15
-18
@@ -193,19 +193,24 @@ class StorageEngine:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
def _ensure_columns(tbl: str, cols: List[Tuple[str, str]]):
|
||||||
|
try:
|
||||||
|
existing = {row[1] for row in conn.execute(f"PRAGMA table_info({tbl})").fetchall()}
|
||||||
|
for col, col_def in cols:
|
||||||
|
if col not in existing:
|
||||||
|
conn.execute(f"ALTER TABLE {tbl} ADD COLUMN {col} {col_def}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Migrations for license_config
|
# Migrations for license_config
|
||||||
for col, col_def in [
|
_ensure_columns("license_config", [
|
||||||
("license_id", "TEXT"),
|
("license_id", "TEXT"),
|
||||||
("license_server_url", "TEXT"),
|
("license_server_url", "TEXT"),
|
||||||
("issued_at", "TEXT"),
|
("issued_at", "TEXT"),
|
||||||
("expires_at", "TEXT"),
|
("expires_at", "TEXT"),
|
||||||
("license_status", "TEXT DEFAULT 'unlicensed'"),
|
("license_status", "TEXT DEFAULT 'unlicensed'"),
|
||||||
("features", "TEXT DEFAULT '[]'")
|
("features", "TEXT DEFAULT '[]'")
|
||||||
]:
|
])
|
||||||
try:
|
|
||||||
conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}")
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. Clients Table
|
# 4. Clients Table
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
@@ -221,7 +226,7 @@ class StorageEngine:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# Migrations for clients table
|
# Migrations for clients table
|
||||||
for col, col_def in [
|
_ensure_columns("clients", [
|
||||||
("metrics_json", "TEXT DEFAULT '{}'"),
|
("metrics_json", "TEXT DEFAULT '{}'"),
|
||||||
("cpu_percent", "REAL DEFAULT 0.0"),
|
("cpu_percent", "REAL DEFAULT 0.0"),
|
||||||
("memory_percent", "REAL DEFAULT 0.0"),
|
("memory_percent", "REAL DEFAULT 0.0"),
|
||||||
@@ -229,11 +234,7 @@ class StorageEngine:
|
|||||||
("client_version", "TEXT DEFAULT ''"),
|
("client_version", "TEXT DEFAULT ''"),
|
||||||
("update_status", "TEXT DEFAULT 'up_to_date'"),
|
("update_status", "TEXT DEFAULT 'up_to_date'"),
|
||||||
("last_update_check", "TEXT")
|
("last_update_check", "TEXT")
|
||||||
]:
|
])
|
||||||
try:
|
|
||||||
conn.execute(f"ALTER TABLE clients ADD COLUMN {col} {col_def}")
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 5. Users Table
|
# 5. Users Table
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
@@ -249,14 +250,10 @@ class StorageEngine:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# Migrations for users table (2FA)
|
# Migrations for users table (2FA)
|
||||||
for col, col_def in [
|
_ensure_columns("users", [
|
||||||
("totp_secret", "TEXT"),
|
("totp_secret", "TEXT"),
|
||||||
("totp_enabled", "INTEGER DEFAULT 0")
|
("totp_enabled", "INTEGER DEFAULT 0")
|
||||||
]:
|
])
|
||||||
try:
|
|
||||||
conn.execute(f"ALTER TABLE users ADD COLUMN {col} {col_def}")
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Ensure default admin user exists
|
# Ensure default admin user exists
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|||||||
+67
-8
@@ -45,6 +45,9 @@ class ServerUpdateManager:
|
|||||||
|
|
||||||
# Hosted client packages cache
|
# Hosted client packages cache
|
||||||
self.hosted_client_version: str = LOGAR_VERSION
|
self.hosted_client_version: str = LOGAR_VERSION
|
||||||
|
self._sha256_cache: Dict[str, Tuple[float, int, str]] = {}
|
||||||
|
self._package_cache: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._package_cache_mtimes: Dict[str, float] = {}
|
||||||
self._ensure_package_structure()
|
self._ensure_package_structure()
|
||||||
|
|
||||||
def _ensure_package_structure(self):
|
def _ensure_package_structure(self):
|
||||||
@@ -52,21 +55,48 @@ class ServerUpdateManager:
|
|||||||
for os_name in ["windows", "linux"]:
|
for os_name in ["windows", "linux"]:
|
||||||
os.makedirs(os.path.join(self.update_dir, os_name), exist_ok=True)
|
os.makedirs(os.path.join(self.update_dir, os_name), exist_ok=True)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def calculate_sha256(filepath: str) -> str:
|
def calculate_sha256(cls, filepath: str) -> str:
|
||||||
"""Computes the SHA-256 digest of a local file."""
|
"""Computes the SHA-256 digest of a local file with stat-based memoization."""
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
return ""
|
return ""
|
||||||
|
try:
|
||||||
|
stat = os.stat(filepath)
|
||||||
|
mtime = stat.st_mtime
|
||||||
|
size = stat.st_size
|
||||||
|
if hasattr(cls, "_global_sha256_cache"):
|
||||||
|
cached = cls._global_sha256_cache.get(filepath)
|
||||||
|
if cached and cached[0] == mtime and cached[1] == size:
|
||||||
|
return cached[2]
|
||||||
|
except Exception:
|
||||||
|
mtime = 0.0
|
||||||
|
size = 0
|
||||||
|
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
with open(filepath, "rb") as f:
|
with open(filepath, "rb") as f:
|
||||||
while chunk := f.read(65536):
|
while chunk := f.read(65536):
|
||||||
h.update(chunk)
|
h.update(chunk)
|
||||||
return h.hexdigest().lower()
|
digest = h.hexdigest().lower()
|
||||||
|
|
||||||
|
if not hasattr(cls, "_global_sha256_cache"):
|
||||||
|
cls._global_sha256_cache = {}
|
||||||
|
cls._global_sha256_cache[filepath] = (mtime, size, digest)
|
||||||
|
return digest
|
||||||
|
|
||||||
def set_hosted_client_version(self, version_str: str):
|
def set_hosted_client_version(self, version_str: str):
|
||||||
"""Sets the version tag for currently hosted client packages."""
|
"""Sets the version tag for currently hosted client packages."""
|
||||||
if version_str:
|
if version_str:
|
||||||
self.hosted_client_version = version_str.strip().lstrip("v")
|
self.hosted_client_version = version_str.strip().lstrip("v")
|
||||||
|
self._package_cache.clear()
|
||||||
|
|
||||||
|
def invalidate_package_cache(self, os_type: Optional[str] = None):
|
||||||
|
"""Clears in-memory package cache."""
|
||||||
|
if os_type:
|
||||||
|
self._package_cache.pop(os_type.lower(), None)
|
||||||
|
self._package_cache_mtimes.pop(os_type.lower(), None)
|
||||||
|
else:
|
||||||
|
self._package_cache.clear()
|
||||||
|
self._package_cache_mtimes.clear()
|
||||||
|
|
||||||
def register_client_package(
|
def register_client_package(
|
||||||
self,
|
self,
|
||||||
@@ -115,16 +145,37 @@ class ServerUpdateManager:
|
|||||||
with open(manifest_path, "w", encoding="utf-8") as mf:
|
with open(manifest_path, "w", encoding="utf-8") as mf:
|
||||||
json.dump(manifest, mf, indent=2)
|
json.dump(manifest, mf, indent=2)
|
||||||
|
|
||||||
|
# Update cache directly
|
||||||
|
cached_entry = dict(manifest)
|
||||||
|
cached_entry["file_path"] = dest_path
|
||||||
|
cached_entry["exists"] = True
|
||||||
|
self._package_cache[norm_os] = cached_entry
|
||||||
|
self._package_cache_mtimes[norm_os] = os.path.getmtime(dest_path)
|
||||||
|
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
def get_hosted_packages(self) -> Dict[str, Dict[str, Any]]:
|
def get_hosted_packages(self, force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns info on currently hosted client packages for Windows and Linux.
|
Returns info on currently hosted client packages for Windows and Linux.
|
||||||
Scans update_dir and falls back to project dist/ directory if present.
|
Uses in-memory cache validated against file modification times to minimize disk I/O.
|
||||||
"""
|
"""
|
||||||
result: Dict[str, Dict[str, Any]] = {}
|
result: Dict[str, Dict[str, Any]] = {}
|
||||||
for os_name in ["windows", "linux"]:
|
for os_name in ["windows", "linux"]:
|
||||||
manifest_path = os.path.join(self.update_dir, os_name, "manifest.json")
|
manifest_path = os.path.join(self.update_dir, os_name, "manifest.json")
|
||||||
|
target_file = None
|
||||||
|
|
||||||
|
if not force_refresh and os_name in self._package_cache:
|
||||||
|
cached = self._package_cache[os_name]
|
||||||
|
tf = cached.get("file_path")
|
||||||
|
if tf and os.path.exists(tf):
|
||||||
|
try:
|
||||||
|
cur_mtime = os.path.getmtime(tf)
|
||||||
|
if cur_mtime == self._package_cache_mtimes.get(os_name):
|
||||||
|
result[os_name] = cached
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if os.path.exists(manifest_path):
|
if os.path.exists(manifest_path):
|
||||||
try:
|
try:
|
||||||
with open(manifest_path, "r", encoding="utf-8") as mf:
|
with open(manifest_path, "r", encoding="utf-8") as mf:
|
||||||
@@ -134,6 +185,8 @@ class ServerUpdateManager:
|
|||||||
info["exists"] = True
|
info["exists"] = True
|
||||||
info["file_path"] = target_file
|
info["file_path"] = target_file
|
||||||
result[os_name] = info
|
result[os_name] = info
|
||||||
|
self._package_cache[os_name] = info
|
||||||
|
self._package_cache_mtimes[os_name] = os.path.getmtime(target_file)
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -164,7 +217,7 @@ class ServerUpdateManager:
|
|||||||
|
|
||||||
if found_pkg and os.path.exists(found_pkg):
|
if found_pkg and os.path.exists(found_pkg):
|
||||||
sha256 = self.calculate_sha256(found_pkg)
|
sha256 = self.calculate_sha256(found_pkg)
|
||||||
result[os_name] = {
|
pkg_data = {
|
||||||
"os": os_name,
|
"os": os_name,
|
||||||
"filename": os.path.basename(found_pkg),
|
"filename": os.path.basename(found_pkg),
|
||||||
"version": self.hosted_client_version or LOGAR_VERSION,
|
"version": self.hosted_client_version or LOGAR_VERSION,
|
||||||
@@ -174,8 +227,11 @@ class ServerUpdateManager:
|
|||||||
"exists": True,
|
"exists": True,
|
||||||
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
}
|
}
|
||||||
|
result[os_name] = pkg_data
|
||||||
|
self._package_cache[os_name] = pkg_data
|
||||||
|
self._package_cache_mtimes[os_name] = os.path.getmtime(found_pkg)
|
||||||
else:
|
else:
|
||||||
result[os_name] = {
|
pkg_data = {
|
||||||
"os": os_name,
|
"os": os_name,
|
||||||
"filename": None,
|
"filename": None,
|
||||||
"version": self.hosted_client_version or LOGAR_VERSION,
|
"version": self.hosted_client_version or LOGAR_VERSION,
|
||||||
@@ -184,6 +240,9 @@ class ServerUpdateManager:
|
|||||||
"file_path": None,
|
"file_path": None,
|
||||||
"exists": False
|
"exists": False
|
||||||
}
|
}
|
||||||
|
result[os_name] = pkg_data
|
||||||
|
self._package_cache[os_name] = pkg_data
|
||||||
|
self._package_cache_mtimes[os_name] = 0.0
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
+47
-2
@@ -137,11 +137,34 @@ class TestUpdateSystem(unittest.TestCase):
|
|||||||
self.assertEqual(chk_resp["latest_version"], "0.3.6.0")
|
self.assertEqual(chk_resp["latest_version"], "0.3.6.0")
|
||||||
|
|
||||||
# 4. Download package endpoint
|
# 4. Download package endpoint
|
||||||
dl_resp = Server.download_client_update_package(os="windows")
|
from starlette.requests import Request as StarletteRequest
|
||||||
|
req_scope = {
|
||||||
|
"type": "http",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/client/update/download",
|
||||||
|
"headers": [],
|
||||||
|
"query_string": b"os=windows"
|
||||||
|
}
|
||||||
|
init_req = StarletteRequest(req_scope)
|
||||||
|
dl_resp = Server.download_client_update_package(request=init_req, os="windows")
|
||||||
self.assertEqual(dl_resp.status_code, 200)
|
self.assertEqual(dl_resp.status_code, 200)
|
||||||
self.assertIn("X-Checksum-SHA256", dl_resp.headers)
|
self.assertIn("X-Checksum-SHA256", dl_resp.headers)
|
||||||
|
self.assertIn("ETag", dl_resp.headers)
|
||||||
|
etag = dl_resp.headers["ETag"]
|
||||||
|
|
||||||
# 5. WebUI rendering verification
|
# 5. Conditional GET with ETag (304 Not Modified)
|
||||||
|
cond_scope = {
|
||||||
|
"type": "http",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/client/update/download",
|
||||||
|
"headers": [(b"if-none-match", etag.encode("utf-8"))],
|
||||||
|
"query_string": b"os=windows"
|
||||||
|
}
|
||||||
|
cached_req = StarletteRequest(cond_scope)
|
||||||
|
cached_resp = Server.download_client_update_package(request=cached_req, os="windows")
|
||||||
|
self.assertEqual(cached_resp.status_code, 304)
|
||||||
|
|
||||||
|
# 6. WebUI rendering verification
|
||||||
# Simulate server update available
|
# Simulate server update available
|
||||||
self.update_mgr.server_update_available = True
|
self.update_mgr.server_update_available = True
|
||||||
self.update_mgr.latest_upstream_version = "0.3.6.0"
|
self.update_mgr.latest_upstream_version = "0.3.6.0"
|
||||||
@@ -155,6 +178,28 @@ class TestUpdateSystem(unittest.TestCase):
|
|||||||
self.assertIn("tabPane-updates", html)
|
self.assertIn("tabPane-updates", html)
|
||||||
self.assertIn("Client Package Distribution Repository", html)
|
self.assertIn("Client Package Distribution Repository", html)
|
||||||
|
|
||||||
|
def test_update_memoization_and_cache_invalidation(self):
|
||||||
|
"""Tests that package info is served from memory cache and invalidates on update."""
|
||||||
|
dummy_pkg = os.path.join(self.test_dir, "LOGAR_Client_Linux")
|
||||||
|
with open(dummy_pkg, "wb") as f:
|
||||||
|
f.write(b"LINUX_CLIENT_PAYLOAD_V1")
|
||||||
|
|
||||||
|
self.update_mgr.register_client_package("linux", dummy_pkg, version="0.3.6.0")
|
||||||
|
pkgs_1 = self.update_mgr.get_hosted_packages()
|
||||||
|
sha1 = pkgs_1["linux"]["sha256"]
|
||||||
|
|
||||||
|
# Repeated call should hit memory cache with identical hash
|
||||||
|
pkgs_2 = self.update_mgr.get_hosted_packages()
|
||||||
|
self.assertEqual(pkgs_2["linux"]["sha256"], sha1)
|
||||||
|
|
||||||
|
# Invalidate cache on new registration
|
||||||
|
with open(dummy_pkg, "wb") as f:
|
||||||
|
f.write(b"LINUX_CLIENT_PAYLOAD_V2_UPDATED")
|
||||||
|
self.update_mgr.register_client_package("linux", dummy_pkg, version="0.3.7.0")
|
||||||
|
pkgs_3 = self.update_mgr.get_hosted_packages()
|
||||||
|
self.assertEqual(pkgs_3["linux"]["version"], "0.3.7.0")
|
||||||
|
self.assertNotEqual(pkgs_3["linux"]["sha256"], sha1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user