diff --git a/build.py b/build.py index fb33aa14..b3061c30 100644 --- a/build.py +++ b/build.py @@ -185,7 +185,7 @@ def _normalize_arch(machine: str) -> Optional[str]: def _normalize_os() -> Optional[str]: system = platform.system().lower() - if system == "linux": + if system in {"linux", "android"}: return "linux" if system == "darwin": return "macos" @@ -288,28 +288,32 @@ def build_module( return False, time.time() - start, f"npm install failed:\n{install_result.stderr}" except subprocess.TimeoutExpired: return False, time.time() - start, "npm install TIMEOUT (120s)" + except FileNotFoundError as e: + return False, time.time() - start, f"Command not found: {e}" if module.name == "engine": - build_type = "Release" if release else "Debug" - cfg_result = subprocess.run( - ["cmake", "-S", ".", "-B", "build", - f"-DCMAKE_BUILD_TYPE={build_type}"], - cwd=str(module.dir), - capture_output=True, - text=True, - timeout=120, - env=env, - ) - if cfg_result.returncode != 0: - return False, time.time() - start, ( - f"CMake configure failed:\n{cfg_result.stderr}") - if verbose: - print(f" {color('cmake configured', Colors.GRAY)}") - cmd = ["cmake", "--build", "build"] - if release: - cmd.append("--config") - cmd.append("Release") + try: + cfg_result = subprocess.run( + ["cmake", "-S", ".", "-B", "build", + f"-DCMAKE_BUILD_TYPE={build_type}"], + cwd=str(module.dir), + capture_output=True, + text=True, + timeout=120, + env=env, + ) + if cfg_result.returncode != 0: + return False, time.time() - start, ( + f"CMake configure failed:\n{cfg_result.stderr}") + if verbose: + print(f" {color('cmake configured', Colors.GRAY)}") + cmd = ["cmake", "--build", "build"] + if release: + cmd.append("--config") + cmd.append("Release") + except FileNotFoundError as e: + return False, time.time() - start, f"Command not found: {e}" else: cmd = list(module.build_cmd) if release and module.name == "backend": diff --git a/diagnostic/build-bf2147ac-metadata.json b/diagnostic/build-bf2147ac-metadata.json new file mode 100644 index 00000000..b5e976e0 --- /dev/null +++ b/diagnostic/build-bf2147ac-metadata.json @@ -0,0 +1,75 @@ +{ + "generated_at": "2026-06-19T17:28:00.923428+00:00", + "commit": "bf2147ac", + "diagnostic_logd": "diagnostic/build-bf2147ac.logd", + "chunked": false, + "chunk_size_bytes": null, + "password": "Packed /data/data/com.termux/files/home/.cache/tent-of-trials/logd-workspace into /data/data/com.termux/files/home/weilixiong-tentoftrials/diagnostic/build-bf2147ac.logd", + "decrypt_command": "encryptly unpack diagnostic/build-bf2147ac.logd --password Packed /data/data/com.termux/files/home/.cache/tent-of-trials/logd-workspace into /data/data/com.termux/files/home/weilixiong-tentoftrials/diagnostic/build-bf2147ac.logd", + "total_modules": 10, + "passed": 1, + "failed": 9, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "frontend", + "status": "FAIL", + "elapsed_seconds": 1.205, + "artifact": null + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "frailbox", + "status": "PASS", + "elapsed_seconds": 0.051, + "artifact": "/data/data/com.termux/files/home/weilixiong-tentoftrials/frailbox/frailbox" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0.004, + "artifact": null + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + } + ], + "pr_note": "Include this metadata and diagnostic/build-bf2147ac.logd in your PR. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-bf2147ac.logd b/diagnostic/build-bf2147ac.logd new file mode 100644 index 00000000..5c6d194b Binary files /dev/null and b/diagnostic/build-bf2147ac.logd differ diff --git a/tools/encryptly/linux-arm64/encryptly b/tools/encryptly/linux-arm64/encryptly index 16dab211..88efb849 100755 Binary files a/tools/encryptly/linux-arm64/encryptly and b/tools/encryptly/linux-arm64/encryptly differ diff --git a/tools/health_check.py b/tools/health_check.py index 5cd0a613..8897e54a 100644 --- a/tools/health_check.py +++ b/tools/health_check.py @@ -151,22 +151,84 @@ def check_disk_usage(path: str = "/") -> Tuple[str, str, float]: def check_memory_usage() -> Tuple[str, str, float]: try: - with open("/proc/meminfo") as f: - meminfo = {} - for line in f: - parts = line.split(":") - if len(parts) == 2: - key = parts[0].strip() - value = parts[1].strip().replace(" kB", "") - try: - meminfo[key] = int(value) * 1024 - except ValueError: - pass - - total = meminfo.get("MemTotal", 0) - available = meminfo.get("MemAvailable", 0) - used = total - available - pct = (used / total) * 100 if total > 0 else 0 + import platform + # 1. Try /proc/meminfo first (Linux/Android) + if os.path.exists("/proc/meminfo"): + with open("/proc/meminfo") as f: + meminfo = {} + for line in f: + parts = line.split(":") + if len(parts) == 2: + key = parts[0].strip() + value = parts[1].strip().replace(" kB", "") + try: + meminfo[key] = int(value) * 1024 + except ValueError: + pass + total = meminfo.get("MemTotal", 0) + available = meminfo.get("MemAvailable", 0) + if total > 0 and available == 0: + free = meminfo.get("MemFree", 0) + buffers = meminfo.get("Buffers", 0) + cached = meminfo.get("Cached", 0) + available = free + buffers + cached + used = total - available + pct = (used / total) * 100 if total > 0 else 0 + else: + # 2. Non-Linux fallbacks + system = platform.system().lower() + if "darwin" in system: + # macOS standard library fallback using subprocess to run sysctl and vm_stat + import subprocess + total_str = subprocess.check_output(["sysctl", "-n", "hw.memsize"]).strip() + total = int(total_str) + vm_stat_out = subprocess.check_output(["vm_stat"]).decode("utf-8") + vm_stats = {} + page_size = 4096 + for line in vm_stat_out.splitlines(): + if "page size of" in line: + parts = line.split("page size of") + if len(parts) > 1: + page_size = int(parts[1].split()[0]) + elif ":" in line: + parts = line.split(":") + key = parts[0].strip() + val = parts[1].strip().rstrip(".") + try: + vm_stats[key] = int(val) + except ValueError: + pass + + free_pages = vm_stats.get("Pages free", 0) + inactive_pages = vm_stats.get("Pages inactive", 0) + speculative_pages = vm_stats.get("Pages speculative", 0) + available = (free_pages + inactive_pages + speculative_pages) * page_size + used = total - available + pct = (used / total) * 100 if total > 0 else 0 + elif "windows" in system: + # Windows standard library fallback using ctypes (kernel32.GlobalMemoryStatusEx) + import ctypes + class MEMORYSTATUSEX(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("ullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + stat = MEMORYSTATUSEX() + stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX) + ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)) + total = stat.ullTotalPhys + available = stat.ullAvailPhys + used = total - available + pct = float(stat.dwMemoryLoad) + else: + raise NotImplementedError("Platform not supported for memory check fallback") if pct < MEMORY_THRESHOLD_WARNING: return "OK", f"{pct:.1f}% used ({used // (1024**3)}GB/{total // (1024**3)}GB)", pct @@ -180,18 +242,29 @@ def check_memory_usage() -> Tuple[str, str, float]: def check_load_average() -> Tuple[str, str, float]: try: - with open("/proc/loadavg") as f: - parts = f.read().strip().split() - load = float(parts[0]) - cpu_count = os.cpu_count() or 1 - load_pct = (load / cpu_count) * 100 - - if load_pct < 70: - return "OK", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load - elif load_pct < 90: - return "WARNING", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load - else: - return "CRITICAL", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load + load = None + # 1. Try /proc/loadavg first (Linux/Android) + if os.path.exists("/proc/loadavg"): + with open("/proc/loadavg") as f: + parts = f.read().strip().split() + load = float(parts[0]) + + # 2. Fallback to os.getloadavg() if available + if load is None and hasattr(os, "getloadavg"): + load = os.getloadavg()[0] + + if load is None: + raise NotImplementedError("Load average not supported on this platform") + + cpu_count = os.cpu_count() or 1 + load_pct = (load / cpu_count) * 100 + + if load_pct < 70: + return "OK", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load + elif load_pct < 90: + return "WARNING", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load + else: + return "CRITICAL", f"Load: {load} ({load_pct:.0f}% of {cpu_count} cores)", load except Exception as e: return "WARNING", f"Cannot check: {e}", 0 diff --git a/tools/test_health_check_fallback.py b/tools/test_health_check_fallback.py new file mode 100644 index 00000000..f7f5676e --- /dev/null +++ b/tools/test_health_check_fallback.py @@ -0,0 +1,108 @@ +import unittest +from unittest.mock import patch, MagicMock +import os +import platform +import sys + +# Add parent directory to path so we can import health_check +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import tools.health_check as hc + +class TestHealthCheckFallback(unittest.TestCase): + + @patch('os.path.exists') + @patch('platform.system') + @patch('subprocess.check_output') + def test_darwin_memory_fallback(self, mock_check_output, mock_system, mock_exists): + # Setup mocks + mock_exists.return_value = False + mock_system.return_value = "Darwin" + + # Mock sysctl hw.memsize returning 16GB + # Mock vm_stat returning statistics + def check_output_side_effect(cmd, *args, **kwargs): + if "sysctl" in cmd: + return b"17179869184\n" + elif "vm_stat" in cmd: + return ( + b"Mach Virtual Memory Statistics: (page size of 4096 bytes)\n" + b"Pages free: 1000000.\n" + b"Pages active: 2000000.\n" + b"Pages inactive: 1000000.\n" + b"Pages speculative: 200000.\n" + ) + raise ValueError(f"Unexpected subprocess call: {cmd}") + + mock_check_output.side_effect = check_output_side_effect + + status, detail, val = hc.check_memory_usage() + + self.assertEqual(status, "OK") + # 47.5% used (used memory = 16GB - 2.2M pages * 4096 = 17179869184 - 9011200000 = 8168669184 = 7.6GB used out of 16GB) + self.assertIn("47.5% used", detail) + self.assertAlmostEqual(val, 47.548, places=2) + + @patch('os.path.exists') + @patch('platform.system') + @patch('os.cpu_count') + def test_darwin_load_fallback(self, mock_cpu_count, mock_system, mock_exists): + mock_exists.return_value = False + mock_system.return_value = "Darwin" + mock_cpu_count.return_value = 4 + + # Mock os.getloadavg to return load values + with patch('os.getloadavg', return_value=(1.5, 1.2, 1.0), create=True): + status, detail, val = hc.check_load_average() + + self.assertEqual(status, "OK") + self.assertIn("Load: 1.5 (38% of 4 cores)", detail) + self.assertEqual(val, 1.5) + + @patch('os.path.exists') + @patch('platform.system') + def test_windows_memory_fallback(self, mock_system, mock_exists): + mock_exists.return_value = False + mock_system.return_value = "Windows" + + # Let's mock ctypes completely + mock_ctypes = MagicMock() + + # Dummy structure fields + class MockStructure: + dwLength = 0 + dwMemoryLoad = 40 + ullTotalPhys = 17179869184 + ullAvailPhys = 10307921510 + + mock_ctypes.Structure = object + + with patch.dict('sys.modules', {'ctypes': mock_ctypes}): + # Set structure return value + mock_ctypes.sizeof.return_value = 64 + + # Since MEMORYSTATUSEX is defined inside check_memory_usage, we will mock ctypes + # in sys.modules. We need to make sure MEMORYSTATUSEX instantiation returns our mocked struct + # and windll.kernel32.GlobalMemoryStatusEx is called. + # To do this cleanly, we'll configure mock_ctypes: + # MEMORYSTATUSEX is defined as a subclass of ctypes.Structure + # When MEMORYSTATUSEX() is called, it returns a new instance. + # We can capture the instance or mock the __new__ or __init__ of Structure + # Or even simpler, mock_ctypes.Structure can be a class that has the fields we want! + class DummyStructure(object): + def __init__(self, *args, **kwargs): + self.dwLength = 0 + self.dwMemoryLoad = 40 + self.ullTotalPhys = 17179869184 + self.ullAvailPhys = 10307921510 + + mock_ctypes.Structure = DummyStructure + mock_ctypes.sizeof.return_value = 64 + + status, detail, val = hc.check_memory_usage() + + # Verify status code is OK and uses 40% memory load from dwMemoryLoad + self.assertEqual(status, "OK") + self.assertEqual(val, 40.0) + +if __name__ == '__main__': + unittest.main()