diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index c5d3586ce..1f3325a8a 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -4,7 +4,6 @@ from .base import ( COOKIES, ENV_MODE_ENV_VAR, IS_WINDOWS, - IS_WINDOWS_BUN_SUPPORTED_MACHINE, # type: ignore LOCAL_STORAGE, POLLING_MAX_HTTP_BUFFER_SIZE, PYTEST_CURRENT_TEST, @@ -87,7 +86,6 @@ __ALL__ = [ Hooks, Imports, IS_WINDOWS, - IS_WINDOWS_BUN_SUPPORTED_MACHINE, LOCAL_STORAGE, LogLevel, MemoizationDisposition, diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 820147ed2..733859ad4 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -11,11 +11,6 @@ from types import SimpleNamespace from platformdirs import PlatformDirs IS_WINDOWS = platform.system() == "Windows" -# https://github.com/oven-sh/bun/blob/main/src/cli/install.ps1 -IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [ - "AMD64", - "x86_64", -] # filter out 32 bit + ARM class Dirs(SimpleNamespace): diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index e240b0aa1..e4aefb8f1 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -280,7 +280,11 @@ def output_system_info(): system = platform.system() - if system != "Windows": + if ( + system != "Windows" + or system == "Windows" + and prerequisites.is_windows_bun_supported() + ): dependencies.extend( [ f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]", diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 32bcc5a93..388bd5388 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -2,6 +2,7 @@ from __future__ import annotations +import functools import glob import importlib import inspect @@ -49,6 +50,14 @@ class Template(Base): demo_url: str +class CpuInfo(Base): + """Model to save cpu info.""" + + manufacturer_id: Optional[str] + model_name: Optional[str] + address_width: Optional[int] + + def check_latest_package_version(package_name: str): """Check if the latest version of the package is installed. @@ -172,7 +181,7 @@ def get_install_package_manager() -> str | None: Returns: The path to the package manager. """ - if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE: + if constants.IS_WINDOWS and not is_windows_bun_supported(): return get_package_manager() return get_config().bun_path @@ -728,7 +737,7 @@ def install_bun(): Raises: FileNotFoundError: If required packages are not found. """ - if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE: + if constants.IS_WINDOWS and not is_windows_bun_supported(): console.warn( "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm." ) @@ -833,7 +842,7 @@ def install_frontend_packages(packages: set[str], config: Config): get_package_manager() if not constants.IS_WINDOWS or constants.IS_WINDOWS - and constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE + and is_windows_bun_supported() else None ) processes.run_process_with_fallback( @@ -1418,3 +1427,92 @@ def initialize_app(app_name: str, template: str | None = None): ) telemetry.send("init", template=template) + + +def format_address_width(address_width) -> int | None: + """Cast address width to an int. + + Args: + address_width: The address width. + + Returns: + Address width int + """ + try: + return int(address_width) if address_width else None + except ValueError: + return None + + +@functools.lru_cache(maxsize=None) +def get_cpu_info() -> CpuInfo | None: + """Get the CPU info of the underlining host. + + Returns: + The CPU info. + """ + platform_os = platform.system() + cpuinfo = {} + try: + if platform_os == "Windows": + cmd = "wmic cpu get addresswidth,caption,manufacturer /FORMAT:csv" + output = processes.execute_command_and_return_output(cmd) + if output: + val = output.splitlines()[-1].split(",")[1:] + cpuinfo["manufacturer_id"] = val[2] + cpuinfo["model_name"] = val[1].split("Family")[0].strip() + cpuinfo["address_width"] = format_address_width(val[0]) + elif platform_os == "Linux": + output = processes.execute_command_and_return_output("lscpu") + if output: + lines = output.split("\n") + for line in lines: + if "Architecture" in line: + cpuinfo["address_width"] = ( + 64 if line.split(":")[1].strip() == "x86_64" else 32 + ) + if "Vendor ID:" in line: + cpuinfo["manufacturer_id"] = line.split(":")[1].strip() + if "Model name" in line: + cpuinfo["model_name"] = line.split(":")[1].strip() + elif platform_os == "Darwin": + cpuinfo["address_width"] = format_address_width( + processes.execute_command_and_return_output("getconf LONG_BIT") + ) + cpuinfo["manufacturer_id"] = processes.execute_command_and_return_output( + "sysctl -n machdep.cpu.brand_string" + ) + cpuinfo["model_name"] = processes.execute_command_and_return_output( + "uname -m" + ) + except Exception as err: + console.error(f"Failed to retrieve CPU info. {err}") + return None + + return ( + CpuInfo( + manufacturer_id=cpuinfo.get("manufacturer_id"), + model_name=cpuinfo.get("model_name"), + address_width=cpuinfo.get("address_width"), + ) + if cpuinfo + else None + ) + + +@functools.lru_cache(maxsize=None) +def is_windows_bun_supported() -> bool: + """Check whether the underlining host running windows qualifies to run bun. + We typically do not run bun on ARM or 32 bit devices that use windows. + + Returns: + Whether the host is qualified to use bun. + """ + cpu_info = get_cpu_info() + return ( + constants.IS_WINDOWS + and cpu_info is not None + and cpu_info.address_width == 64 + and cpu_info.model_name is not None + and "ARM" not in cpu_info.model_name + ) diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index dfe3f5cc6..f63e1c709 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -347,3 +347,21 @@ def run_process_with_fallback(args, *, show_status_message, fallback=None, **kwa fallback=None, **kwargs, ) + + +def execute_command_and_return_output(command) -> str | None: + """Execute a command and return the output. + + Args: + command: The command to run. + + Returns: + The output of the command. + """ + try: + return subprocess.check_output(command, shell=True).decode().strip() + except subprocess.SubprocessError as err: + console.error( + f"The command `{command}` failed with error: {err}. This will return None." + ) + return None diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index fc56ca3ec..e38c4fe79 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -32,6 +32,15 @@ def get_os() -> str: return platform.system() +def get_detailed_platform_str() -> str: + """Get the detailed os/platform string. + + Returns: + The platform string + """ + return platform.platform() + + def get_python_version() -> str: """Get the Python version. @@ -97,6 +106,8 @@ def _prepare_event(event: str, **kwargs) -> dict: Returns: The event data. """ + from reflex.utils.prerequisites import get_cpu_info + installation_id = ensure_reflex_installation_id() project_hash = get_project_hash(raise_on_fail=_raise_on_missing_project_hash()) @@ -112,6 +123,9 @@ def _prepare_event(event: str, **kwargs) -> dict: else: # for python 3.11 & 3.12 stamp = datetime.now(UTC).isoformat() + + cpuinfo = get_cpu_info() + return { "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb", "event": event, @@ -119,10 +133,12 @@ def _prepare_event(event: str, **kwargs) -> dict: "distinct_id": installation_id, "distinct_app_id": project_hash, "user_os": get_os(), + "user_os_detail": get_detailed_platform_str(), "reflex_version": get_reflex_version(), "python_version": get_python_version(), "cpu_count": get_cpu_count(), "memory": get_memory(), + "cpu_info": dict(cpuinfo) if cpuinfo else {}, **( {"template": template} if (template := kwargs.get("template")) is not None @@ -165,5 +181,4 @@ def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool: event_data = _prepare_event(event, **kwargs) if not event_data: return False - return _send_event(event_data) diff --git a/tests/test_prerequisites.py b/tests/test_prerequisites.py index 28608c48c..c4f57a998 100644 --- a/tests/test_prerequisites.py +++ b/tests/test_prerequisites.py @@ -8,8 +8,10 @@ import pytest from reflex import constants from reflex.config import Config from reflex.utils.prerequisites import ( + CpuInfo, _update_next_config, cached_procedure, + get_cpu_info, initialize_requirements_txt, ) @@ -203,3 +205,14 @@ def test_cached_procedure(): assert call_count == 2 _function_with_some_args(100, y=300) assert call_count == 2 + + +def test_get_cpu_info(): + cpu_info = get_cpu_info() + assert cpu_info is not None + assert isinstance(cpu_info, CpuInfo) + assert cpu_info.model_name is not None + + for attr in ("manufacturer_id", "model_name", "address_width"): + value = getattr(cpu_info, attr) + assert value.strip() if attr != "address_width" else value diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 2cbbe0ee8..58786c67b 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -41,6 +41,7 @@ def test_send(mocker, event): read_data='{"project_hash": "78285505863498957834586115958872998605"}' ), ) + mocker.patch("platform.platform", return_value="Mocked Platform") telemetry.send(event, telemetry_enabled=True) httpx.post.assert_called_once()