Spaces:
Running
Running
| from __future__ import annotations | |
| import os | |
| import shutil | |
| import subprocess | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Callable, Mapping, MutableMapping, Optional | |
| from dotenv import set_key | |
| AUTOSTART_ENV_KEY = "AIFORECAST_START_WITH_WINDOWS" | |
| AUTOSTART_REGISTRY_VALUE_NAME = "SuperAIForecastRunBat" | |
| AUTOSTART_STARTUP_FILE_NAME = "SuperAIForecastRunBat.cmd" | |
| BROWSER_PATH_ENV_KEY = "AIFORECAST_BROWSER_PATH" | |
| WINDOWS_RUN_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run" | |
| DEFAULT_BROWSER_PREFERENCE = "opera" | |
| class BrowserLaunchStatus: | |
| preferred: str | |
| available: bool | |
| executable: Optional[str] | |
| def to_payload(self) -> dict[str, object]: | |
| return { | |
| "preferred": self.preferred, | |
| "available": self.available, | |
| "executable": self.executable, | |
| } | |
| class StartupPreferenceStatus: | |
| supported: bool | |
| enabled: bool | |
| registered: bool | |
| command: Optional[str] | |
| env_key: str | |
| registry_value_name: str | |
| browser: BrowserLaunchStatus | |
| message: str | |
| def to_payload(self) -> dict[str, object]: | |
| return { | |
| "supported": self.supported, | |
| "enabled": self.enabled, | |
| "registered": self.registered, | |
| "command": self.command, | |
| "env_key": self.env_key, | |
| "registry_value_name": self.registry_value_name, | |
| "browser": self.browser.to_payload(), | |
| "message": self.message, | |
| } | |
| def _read_bool_env_value(value: Optional[str], *, default: bool) -> bool: | |
| if value is None: | |
| return default | |
| normalized = value.strip().lower() | |
| if not normalized: | |
| return default | |
| if normalized in {"1", "true", "yes", "on"}: | |
| return True | |
| if normalized in {"0", "false", "no", "off"}: | |
| return False | |
| return default | |
| def resolve_start_with_windows_enabled( | |
| env: Mapping[str, str] | None = None, | |
| *, | |
| default: bool = True, | |
| ) -> bool: | |
| runtime_env = os.environ if env is None else env | |
| return _read_bool_env_value(runtime_env.get(AUTOSTART_ENV_KEY), default=default) | |
| def resolve_windows_startup_folder(env: Mapping[str, str] | None = None) -> Path: | |
| runtime_env = os.environ if env is None else env | |
| app_data = runtime_env.get("APPDATA", "").strip() | |
| if app_data: | |
| return Path(app_data) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" | |
| return ( | |
| Path.home() | |
| / "AppData" | |
| / "Roaming" | |
| / "Microsoft" | |
| / "Windows" | |
| / "Start Menu" | |
| / "Programs" | |
| / "Startup" | |
| ) | |
| def build_windows_startup_launcher_path( | |
| project_root: Path, | |
| env: Mapping[str, str] | None = None, | |
| ) -> Path: | |
| del project_root | |
| return resolve_windows_startup_folder(env) / AUTOSTART_STARTUP_FILE_NAME | |
| def build_windows_startup_launcher_contents(project_root: Path) -> str: | |
| resolved_project_root = Path(project_root).resolve() | |
| run_bat_path = resolved_project_root / "run.bat" | |
| return "\n".join( | |
| [ | |
| "@echo off", | |
| f'start "" /d "{resolved_project_root}" "{run_bat_path}"', | |
| "", | |
| ], | |
| ) | |
| def _read_text_file(path: Path) -> Optional[str]: | |
| try: | |
| return path.read_text(encoding="utf-8") | |
| except FileNotFoundError: | |
| return None | |
| def _write_text_file(path: Path, content: str) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(content, encoding="utf-8") | |
| def _delete_file(path: Path) -> None: | |
| try: | |
| path.unlink() | |
| except FileNotFoundError: | |
| return | |
| def _normalize_text_lines(value: Optional[str]) -> Optional[str]: | |
| if value is None: | |
| return None | |
| return value.replace("\r\n", "\n").replace("\r", "\n") | |
| def iter_preferred_browser_candidates(env: Mapping[str, str] | None = None) -> list[str]: | |
| runtime_env = os.environ if env is None else env | |
| candidates: list[str] = [] | |
| custom_browser_path = runtime_env.get(BROWSER_PATH_ENV_KEY, "").strip() | |
| if custom_browser_path: | |
| candidates.append(custom_browser_path) | |
| local_app_data = runtime_env.get("LOCALAPPDATA", "").strip() | |
| program_files = runtime_env.get("ProgramFiles", "").strip() | |
| program_files_x86 = runtime_env.get("ProgramFiles(x86)", "").strip() | |
| common_candidates = [ | |
| Path(local_app_data) / "Programs" / "Opera" / "opera.exe", | |
| Path(local_app_data) / "Programs" / "Opera" / "launcher.exe", | |
| Path(program_files) / "Opera" / "opera.exe", | |
| Path(program_files) / "Opera" / "launcher.exe", | |
| Path(program_files_x86) / "Opera" / "opera.exe", | |
| Path(program_files_x86) / "Opera" / "launcher.exe", | |
| Path(local_app_data) / "Programs" / "Opera GX" / "opera.exe", | |
| Path(local_app_data) / "Programs" / "Opera GX" / "launcher.exe", | |
| ] | |
| candidates.extend(str(path) for path in common_candidates if str(path).strip()) | |
| for binary_name in ("opera.exe", "opera", "launcher.exe"): | |
| binary_path = shutil.which(binary_name) | |
| if binary_path: | |
| candidates.append(binary_path) | |
| deduped: list[str] = [] | |
| seen: set[str] = set() | |
| for candidate in candidates: | |
| normalized = str(candidate).strip() | |
| if not normalized: | |
| continue | |
| lowered = normalized.lower() | |
| if lowered in seen: | |
| continue | |
| seen.add(lowered) | |
| deduped.append(normalized) | |
| return deduped | |
| def get_browser_launch_status( | |
| env: Mapping[str, str] | None = None, | |
| *, | |
| path_exists: Callable[[Path], bool] | None = None, | |
| ) -> BrowserLaunchStatus: | |
| exists = path_exists or Path.exists | |
| for candidate in iter_preferred_browser_candidates(env): | |
| candidate_path = Path(candidate) | |
| if exists(candidate_path): | |
| return BrowserLaunchStatus( | |
| preferred=DEFAULT_BROWSER_PREFERENCE, | |
| available=True, | |
| executable=str(candidate_path), | |
| ) | |
| return BrowserLaunchStatus( | |
| preferred=DEFAULT_BROWSER_PREFERENCE, | |
| available=False, | |
| executable=None, | |
| ) | |
| def open_url_with_preferred_browser( | |
| url: str, | |
| *, | |
| env: Mapping[str, str] | None = None, | |
| path_exists: Callable[[Path], bool] | None = None, | |
| popen: Callable[..., object] = subprocess.Popen, | |
| ) -> bool: | |
| browser_status = get_browser_launch_status(env, path_exists=path_exists) | |
| if not browser_status.available or not browser_status.executable: | |
| return False | |
| try: | |
| popen( | |
| [browser_status.executable, url], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| return True | |
| except OSError: | |
| return False | |
| def _read_windows_run_value( | |
| value_name: str, | |
| *, | |
| key_path: str = WINDOWS_RUN_KEY_PATH, | |
| ) -> Optional[str]: | |
| import winreg | |
| try: | |
| with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_READ) as key: | |
| value, _ = winreg.QueryValueEx(key, value_name) | |
| return str(value) | |
| except FileNotFoundError: | |
| return None | |
| def _delete_windows_run_value( | |
| value_name: str, | |
| *, | |
| key_path: str = WINDOWS_RUN_KEY_PATH, | |
| ) -> None: | |
| import winreg | |
| try: | |
| with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE) as key: | |
| winreg.DeleteValue(key, value_name) | |
| except FileNotFoundError: | |
| return | |
| def get_startup_preference_status( | |
| project_root: Path, | |
| *, | |
| env: Mapping[str, str] | None = None, | |
| os_name: Optional[str] = None, | |
| registry_reader: Callable[[str], Optional[str]] = _read_windows_run_value, | |
| path_exists: Callable[[Path], bool] | None = None, | |
| startup_file_reader: Callable[[Path], Optional[str]] = _read_text_file, | |
| ) -> StartupPreferenceStatus: | |
| resolved_os_name = os.name if os_name is None else os_name | |
| runtime_env = os.environ if env is None else env | |
| supported = resolved_os_name == "nt" | |
| enabled = resolve_start_with_windows_enabled(runtime_env, default=True) | |
| launcher_path = build_windows_startup_launcher_path(project_root, runtime_env) | |
| launcher_contents = _normalize_text_lines(build_windows_startup_launcher_contents(project_root)) | |
| command = str((Path(project_root).resolve() / "run.bat")) | |
| registered_launcher = bool( | |
| supported | |
| and _normalize_text_lines(startup_file_reader(launcher_path)) == launcher_contents | |
| ) | |
| legacy_registered_command = registry_reader(AUTOSTART_REGISTRY_VALUE_NAME) if supported else None | |
| legacy_registered = bool(supported and legacy_registered_command) | |
| registered = bool(supported and registered_launcher and not legacy_registered) | |
| browser = get_browser_launch_status(runtime_env, path_exists=path_exists) | |
| if not supported: | |
| message = "Tu dong khoi dong file .bat chi duoc ho tro tren Windows." | |
| elif enabled and registered_launcher and legacy_registered: | |
| message = "Phat hien logic khoi dong cu trong Windows Run. Launcher se xoa no va giu Startup folder cho run.bat." | |
| elif enabled and registered: | |
| message = "run.bat dang duoc dang ky trong Startup folder cua Windows." | |
| elif enabled: | |
| message = "Da bat tu dong khoi dong. Launcher se tao file Startup mo run.bat." | |
| else: | |
| message = "run.bat se khong khoi dong cung Windows." | |
| return StartupPreferenceStatus( | |
| supported=supported, | |
| enabled=enabled, | |
| registered=registered, | |
| command=command, | |
| env_key=AUTOSTART_ENV_KEY, | |
| registry_value_name=AUTOSTART_REGISTRY_VALUE_NAME, | |
| browser=browser, | |
| message=message, | |
| ) | |
| def sync_startup_preference( | |
| project_root: Path, | |
| *, | |
| env: MutableMapping[str, str] | None = None, | |
| os_name: Optional[str] = None, | |
| registry_reader: Callable[[str], Optional[str]] = _read_windows_run_value, | |
| registry_deleter: Callable[[str], None] = _delete_windows_run_value, | |
| path_exists: Callable[[Path], bool] | None = None, | |
| startup_file_reader: Callable[[Path], Optional[str]] = _read_text_file, | |
| startup_file_writer: Callable[[Path, str], None] = _write_text_file, | |
| startup_file_deleter: Callable[[Path], None] = _delete_file, | |
| ) -> StartupPreferenceStatus: | |
| runtime_env = os.environ if env is None else env | |
| status = get_startup_preference_status( | |
| project_root, | |
| env=runtime_env, | |
| os_name=os_name, | |
| registry_reader=registry_reader, | |
| path_exists=path_exists, | |
| startup_file_reader=startup_file_reader, | |
| ) | |
| if status.supported: | |
| launcher_path = build_windows_startup_launcher_path(project_root, runtime_env) | |
| launcher_contents = build_windows_startup_launcher_contents(project_root) | |
| registry_deleter(status.registry_value_name) | |
| if status.enabled: | |
| startup_file_writer(launcher_path, launcher_contents) | |
| else: | |
| startup_file_deleter(launcher_path) | |
| return get_startup_preference_status( | |
| project_root, | |
| env=runtime_env, | |
| os_name=os_name, | |
| registry_reader=registry_reader, | |
| path_exists=path_exists, | |
| startup_file_reader=startup_file_reader, | |
| ) | |
| def update_startup_preference( | |
| project_root: Path, | |
| env_file: Path, | |
| *, | |
| enabled: bool, | |
| env: MutableMapping[str, str] | None = None, | |
| os_name: Optional[str] = None, | |
| env_writer: Callable[..., tuple[Optional[bool], str, str]] = set_key, | |
| registry_reader: Callable[[str], Optional[str]] = _read_windows_run_value, | |
| registry_deleter: Callable[[str], None] = _delete_windows_run_value, | |
| path_exists: Callable[[Path], bool] | None = None, | |
| startup_file_reader: Callable[[Path], Optional[str]] = _read_text_file, | |
| startup_file_writer: Callable[[Path, str], None] = _write_text_file, | |
| startup_file_deleter: Callable[[Path], None] = _delete_file, | |
| ) -> StartupPreferenceStatus: | |
| runtime_env = os.environ if env is None else env | |
| runtime_env[AUTOSTART_ENV_KEY] = "1" if enabled else "0" | |
| env_file.parent.mkdir(parents=True, exist_ok=True) | |
| env_writer(str(env_file), AUTOSTART_ENV_KEY, runtime_env[AUTOSTART_ENV_KEY], quote_mode="never") | |
| return sync_startup_preference( | |
| project_root, | |
| env=runtime_env, | |
| os_name=os_name, | |
| registry_reader=registry_reader, | |
| registry_deleter=registry_deleter, | |
| path_exists=path_exists, | |
| startup_file_reader=startup_file_reader, | |
| startup_file_writer=startup_file_writer, | |
| startup_file_deleter=startup_file_deleter, | |
| ) | |