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
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.
160 lines
5.1 KiB
Python
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
|