Files
javis_bot/src/desktop_app/cuda_recovery.py
javis-bot c4abf63f38
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Transform isair/jarvis into a Discord-controlled voice assistant running on
the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact.

- bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral),
  voice channel join + voice receive/playback, pluggable VNC screen broadcast
  (selfbot live / noVNC / screenshot)
- bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS
  behind a thin localhost HTTP API
- .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite,
  docs/language-comparison.md and docs/vnc-xfce-setup.md

Language decision: hybrid (Python brain + Node/bun Discord layer) because
Discord blocks bot video; native screen broadcast only works via a Node
selfbot library.
2026-06-09 14:51:05 +09:00

160 lines
5.1 KiB
Python

"""Recovery action for the GPU acceleration libraries on Windows.
The Inno Setup installer ships a PowerShell script (`install_cuda.ps1`) that
downloads cuBLAS and cuDNN into `{app}\\cuda`. That step runs once during
install and may fail silently — slow connections truncate the 643 MB cuDNN
wheel, AV quarantines the unsigned engines DLL, the user dismisses a UAC
prompt. When that happens the runtime probe in `jarvis.listening.listener`
falls back to CPU and the only documented fix used to be "reinstall the app",
which doesn't help because the `.cuda_installed` marker tricks the installer
into skipping the CUDA step.
This module exposes a tray menu action that re-runs the installer script
directly, with UAC elevation, so users can recover without touching the
installer at all.
"""
from __future__ import annotations
import functools
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass(frozen=True)
class CudaRecoveryAction:
label: str
script_path: Path
target_dir: Path
executable: str
arguments: list[str]
@functools.lru_cache(maxsize=None)
def _has_nvidia_driver() -> bool:
"""Match the Inno Setup HasNvidiaGPU check: nvcuda.dll in System32.
Cached because drivers don't appear or disappear during a process run.
"""
if sys.platform != "win32":
return False
system_root = os.environ.get("SystemRoot", r"C:\Windows")
return Path(system_root, "System32", "nvcuda.dll").exists()
def _powershell_executable() -> str:
system_root = os.environ.get("SystemRoot", r"C:\Windows")
return str(
Path(system_root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
)
def cuda_recovery_action(install_root: Path) -> Optional[CudaRecoveryAction]:
"""Return a recovery action if the host platform supports it.
`install_root` is the directory containing `install_cuda.ps1` (in
bundled mode this is the directory next to the frozen executable).
Returns `None` when:
- The platform isn't Windows.
- No NVIDIA driver is detected (nothing to recover to).
- The installer-bundled script is missing (dev runs from source).
"""
if sys.platform != "win32":
return None
if not _has_nvidia_driver():
return None
script_path = Path(install_root) / "install_cuda.ps1"
if not script_path.exists():
return None
target_dir = Path(install_root) / "cuda"
log_path = target_dir / "install.log"
arguments = [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(script_path),
"-TargetDir",
str(target_dir),
"-LogPath",
str(log_path),
]
return CudaRecoveryAction(
label="🎮 Reinstall GPU libraries",
script_path=script_path,
target_dir=target_dir,
executable=_powershell_executable(),
arguments=arguments,
)
def _shell_execute(hwnd: int, verb: str, file: str, params: str, directory: str, show: int) -> int:
"""Thin wrapper over ShellExecuteW so tests can patch it without dragging in ctypes."""
import ctypes
return int(
ctypes.windll.shell32.ShellExecuteW(hwnd, verb, file, params, directory, show)
)
def _quote_arg(arg: str) -> str:
"""Quote a single argument for ShellExecuteW's lpParameters string.
Windows argv parsing (CommandLineToArgvW) treats a backslash run only as
an escape when it precedes a quote: 2n backslashes + " emits n
backslashes and ends the quoted string; 2n+1 emits n + a literal ".
A trailing backslash inside `"..."` therefore swallows the closing
quote unless it is doubled. Doubling every trailing backslash is the
canonical fix and is what argv parsers expect.
"""
if not arg:
return '""'
if not any(ch in arg for ch in (" ", "\t", '"')):
return arg
out: list[str] = ['"']
i = 0
while i < len(arg):
bs = 0
while i < len(arg) and arg[i] == "\\":
bs += 1
i += 1
if i == len(arg):
out.append("\\" * (bs * 2))
break
if arg[i] == '"':
out.append("\\" * (bs * 2 + 1))
out.append('"')
else:
out.append("\\" * bs)
out.append(arg[i])
i += 1
out.append('"')
return "".join(out)
def run_action(action: CudaRecoveryAction) -> bool:
"""Launch the recovery script with UAC elevation.
`install_cuda.ps1` writes into `Program Files\\Jarvis\\cuda`, which a
standard user account cannot write to. ShellExecuteW with the `runas`
verb triggers the UAC prompt; without it the script silently fails
its first file write and the user is no better off than before.
"""
if sys.platform != "win32":
return False
params = " ".join(_quote_arg(a) for a in action.arguments)
rc = _shell_execute(0, "runas", action.executable, params, str(action.target_dir.parent), 1)
# ShellExecuteW returns >32 on success; <=32 means an error code (e.g.
# SE_ERR_ACCESSDENIED 5 when the user dismisses the UAC prompt).
return rc > 32