mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
Fix Windows Cookbook background tasks, exit statuses, and empty SSH logs wrapper (#1389)
This commit consolidates all Windows Cookbook background fixes into a single comprehensive commit based on the latest main branch. Key fixes included: 1. React looksSuccessful Mismatch: Append 'DOWNLOAD_OK' for pip install commands in routes/cookbook_routes.py. 2. Local Windows SSH Wrapper & Log Directory Mismatch: Bypassed ssh wrappers and dynamically selected odysseus-tmux logs for local tasks in static/js/cookbookRunning.js. 3. WSL Bash Filtration: Filtered out the WSL bash stub at C:\Windows\System32\bash.exe in core/platform_compat.py. 4. Drive-Colon Path Normalization: Replaced .as_posix() with git_bash_path() in routes/shell_routes.py and src/bg_jobs.py. 5. GGUF-Only Hardware Fitting: Restructured local Windows recommendations to rank GGUF only in services/hwfit/fit.py. 6. Safe Win32 Process Liveness Probe: Replaced os.kill(pid, 0) with a safe Win32 API probe using GetExitCodeProcess in core/platform_compat.py. 7. Prebuilt llama-cpp-python Wheels: Supply the CPU extra index during compilation failure fallback. 8. Enforce UTF-8 log encoding: Set PYTHONIOENCODING=utf-8 on Windows bootstrap runners. 9. Fix Linux Llama.cpp Build script syntax error in routes/cookbook_helpers.py. 10. Page Reload Status Check: Run sys.executable instead of 'python3' to bypass Microsoft Store execution stubs on local Windows hosts. 11. Llama.cpp serve build bypass: Bypassed cmake compilation checks on local Windows and verified python bindings directly. 12. Serve Command Path Validation: Masked safe GGUF path printf subshells '' inside the serve command validator. 13. CPU Mismatch Diagnostics: Intercepted AVX2-lacking '0xc000001d' (Illegal Instruction) crashes in static/js/cookbook-diagnosis.js and guided users to Ollama. 14. Windows Pytest stability: Fixed stub import leakage in test files.
This commit is contained in:
@@ -180,6 +180,21 @@ def _is_windows_bash_stub(path: str) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def git_bash_path(path: str | Path) -> str:
|
||||||
|
"""Convert a path to POSIX style suitable for Git Bash on Windows.
|
||||||
|
|
||||||
|
Transforms drive letters (e.g., 'C:\\path') to POSIX '/c/path',
|
||||||
|
and uses forward slashes.
|
||||||
|
"""
|
||||||
|
p = Path(path)
|
||||||
|
p_str = p.as_posix()
|
||||||
|
if IS_WINDOWS and len(p_str) >= 2 and p_str[1] == ":":
|
||||||
|
drive = p_str[0].lower()
|
||||||
|
return f"/{drive}{p_str[2:]}"
|
||||||
|
return p_str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def find_bash() -> Optional[str]:
|
def find_bash() -> Optional[str]:
|
||||||
"""Locate a real ``bash`` interpreter, or None.
|
"""Locate a real ``bash`` interpreter, or None.
|
||||||
|
|
||||||
|
|||||||
@@ -206,12 +206,16 @@ def _pip_install_fallback_chain(package: str, *, python_cmd: str = "python3 -m p
|
|||||||
exit code is preserved (no ``| tail`` masking) and the last 5 lines of
|
exit code is preserved (no ``| tail`` masking) and the last 5 lines of
|
||||||
pip output appear in the Cookbook log on failure.
|
pip output appear in the Cookbook log on failure.
|
||||||
"""
|
"""
|
||||||
|
from core.platform_compat import IS_WINDOWS
|
||||||
upgrade_flag = " -U" if upgrade else ""
|
upgrade_flag = " -U" if upgrade else ""
|
||||||
# Shell-quote the package spec: an extras spec like ``llama-cpp-python[server]``
|
# Shell-quote the package spec: an extras spec like ``llama-cpp-python[server]``
|
||||||
# contains brackets that bash would treat as a glob, so it must be quoted
|
# contains brackets that bash would treat as a glob, so it must be quoted
|
||||||
# before being embedded in the install command. Plain names (e.g.
|
# before being embedded in the install command. Plain names (e.g.
|
||||||
# ``huggingface_hub``) are returned unchanged by ``shlex.quote``.
|
# ``huggingface_hub``) are returned unchanged by ``shlex.quote``.
|
||||||
pkg = shlex.quote(package)
|
pkg = shlex.quote(package)
|
||||||
|
if IS_WINDOWS and "llama-cpp-python" in package:
|
||||||
|
pkg += " --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu"
|
||||||
|
|
||||||
base = _pip_install_attempt(f"{python_cmd} install -q{upgrade_flag} {pkg}")
|
base = _pip_install_attempt(f"{python_cmd} install -q{upgrade_flag} {pkg}")
|
||||||
user = _pip_install_attempt(f"{python_cmd} install --user --break-system-packages -q{upgrade_flag} {pkg}")
|
user = _pip_install_attempt(f"{python_cmd} install --user --break-system-packages -q{upgrade_flag} {pkg}")
|
||||||
# Derive the python executable for the venv detection check.
|
# Derive the python executable for the venv detection check.
|
||||||
@@ -525,6 +529,7 @@ def _validate_serve_cmd(v: str | None) -> str | None:
|
|||||||
# Backticks and raw newlines are never legitimate here.
|
# Backticks and raw newlines are never legitimate here.
|
||||||
if any(c in v for c in ("`", "\n", "\r")):
|
if any(c in v for c in ("`", "\n", "\r")):
|
||||||
raise HTTPException(400, "Invalid characters in cmd")
|
raise HTTPException(400, "Invalid characters in cmd")
|
||||||
|
|
||||||
# Known GGUF launcher prelude → validate the serve invocation(s) it guards.
|
# Known GGUF launcher prelude → validate the serve invocation(s) it guards.
|
||||||
m = _GGUF_PRELUDE_RE.match(v)
|
m = _GGUF_PRELUDE_RE.match(v)
|
||||||
if m:
|
if m:
|
||||||
@@ -533,9 +538,19 @@ def _validate_serve_cmd(v: str | None) -> str | None:
|
|||||||
for part in rest.split("||"):
|
for part in rest.split("||"):
|
||||||
_check_serve_binary(part.strip())
|
_check_serve_binary(part.strip())
|
||||||
return v
|
return v
|
||||||
|
|
||||||
# Otherwise: a single invocation — no shell metacharacters allowed.
|
# Otherwise: a single invocation — no shell metacharacters allowed.
|
||||||
|
# Temporarily replace safe $(printf %s ...) expressions with a placeholder
|
||||||
|
# to avoid triggering the metacharacter/command-injection checks.
|
||||||
|
cleaned_v = v
|
||||||
|
printf_matches = list(re.finditer(r"\$\(\s*printf\s+%s\s+([^\n()]*?)\)", v))
|
||||||
|
for match in printf_matches:
|
||||||
|
inner = match.group(1)
|
||||||
|
if not any(c in inner for c in (";", "&&", "||", "$(", "`")):
|
||||||
|
cleaned_v = cleaned_v.replace(match.group(0), "/placeholder/safe/path.gguf")
|
||||||
|
|
||||||
# (`$(` was the original intent; bare `$` is fine for shell-safe paths.)
|
# (`$(` was the original intent; bare `$` is fine for shell-safe paths.)
|
||||||
if any(c in v for c in (";", "&&", "||", "$(")):
|
if any(c in cleaned_v for c in (";", "&&", "||", "$(")):
|
||||||
raise HTTPException(400, "Invalid characters in cmd")
|
raise HTTPException(400, "Invalid characters in cmd")
|
||||||
_check_serve_binary(v)
|
_check_serve_binary(v)
|
||||||
return v
|
return v
|
||||||
|
|||||||
+75
-43
@@ -22,6 +22,7 @@ from core.platform_compat import (
|
|||||||
IS_WINDOWS,
|
IS_WINDOWS,
|
||||||
detached_popen_kwargs,
|
detached_popen_kwargs,
|
||||||
find_bash,
|
find_bash,
|
||||||
|
git_bash_path,
|
||||||
kill_process_tree,
|
kill_process_tree,
|
||||||
pid_alive,
|
pid_alive,
|
||||||
safe_chmod,
|
safe_chmod,
|
||||||
@@ -175,6 +176,7 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
safe_chmod(key_path.with_suffix(".pub"), 0o644)
|
safe_chmod(key_path.with_suffix(".pub"), 0o644)
|
||||||
return {"ok": True, "public_key": _read_cookbook_public_key()}
|
return {"ok": True, "public_key": _read_cookbook_public_key()}
|
||||||
|
|
||||||
|
|
||||||
def _needs_binary(cmd: str, binary: str) -> bool:
|
def _needs_binary(cmd: str, binary: str) -> bool:
|
||||||
return bool(re.search(rf"(^|[\s;&|()]){re.escape(binary)}($|[\s;&|()])", cmd or ""))
|
return bool(re.search(rf"(^|[\s;&|()]){re.escape(binary)}($|[\s;&|()])", cmd or ""))
|
||||||
|
|
||||||
@@ -235,8 +237,8 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
# POSIX form + shell-quoting so drive paths / spaces survive.
|
# POSIX form + shell-quoting so drive paths / spaces survive.
|
||||||
inner = TMUX_LOG_DIR / f"{session_id}_run.sh"
|
inner = TMUX_LOG_DIR / f"{session_id}_run.sh"
|
||||||
inner.write_text("\n".join(bash_lines) + "\n", encoding="utf-8")
|
inner.write_text("\n".join(bash_lines) + "\n", encoding="utf-8")
|
||||||
lp = shlex.quote(log_path.as_posix())
|
lp = shlex.quote(git_bash_path(log_path))
|
||||||
ip = shlex.quote(inner.as_posix())
|
ip = shlex.quote(git_bash_path(inner))
|
||||||
script_path = TMUX_LOG_DIR / f"{session_id}.sh"
|
script_path = TMUX_LOG_DIR / f"{session_id}.sh"
|
||||||
script_path.write_text(
|
script_path.write_text(
|
||||||
f"bash {ip} > {lp} 2>&1\n",
|
f"bash {ip} > {lp} 2>&1\n",
|
||||||
@@ -352,6 +354,8 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
ps_lines = []
|
ps_lines = []
|
||||||
ps_lines.append('$sessionDir = "$env:TEMP\\odysseus-sessions"')
|
ps_lines.append('$sessionDir = "$env:TEMP\\odysseus-sessions"')
|
||||||
ps_lines.append('New-Item -ItemType Directory -Force -Path $sessionDir | Out-Null')
|
ps_lines.append('New-Item -ItemType Directory -Force -Path $sessionDir | Out-Null')
|
||||||
|
ps_lines.append('$env:PYTHONIOENCODING = "utf-8"')
|
||||||
|
ps_lines.append('$env:PYTHONUTF8 = "1"')
|
||||||
if req.hf_token:
|
if req.hf_token:
|
||||||
ps_lines.append(f"$env:HF_TOKEN = '{_ps_squote(req.hf_token)}'")
|
ps_lines.append(f"$env:HF_TOKEN = '{_ps_squote(req.hf_token)}'")
|
||||||
if req.env_prefix:
|
if req.env_prefix:
|
||||||
@@ -851,6 +855,16 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
in_venv=sys.prefix != sys.base_prefix,
|
in_venv=sys.prefix != sys.base_prefix,
|
||||||
)
|
)
|
||||||
is_pip_install = bool(req.cmd and "pip install" in req.cmd)
|
is_pip_install = bool(req.cmd and "pip install" in req.cmd)
|
||||||
|
remote = req.remote_host
|
||||||
|
is_windows = req.platform == "windows"
|
||||||
|
local_windows = IS_WINDOWS and not remote
|
||||||
|
if is_windows or local_windows:
|
||||||
|
if req.cmd.startswith("python3 "):
|
||||||
|
req.cmd = "python " + req.cmd[len("python3 "):]
|
||||||
|
if is_pip_install and ("llama-cpp-python" in req.cmd or "llama_cpp" in req.cmd) and (is_windows or local_windows):
|
||||||
|
if "--extra-index-url" not in req.cmd:
|
||||||
|
req.cmd += " --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu"
|
||||||
|
|
||||||
if is_pip_install:
|
if is_pip_install:
|
||||||
# Keep big dependency wheel builds (vLLM, …) off the home filesystem's
|
# Keep big dependency wheel builds (vLLM, …) off the home filesystem's
|
||||||
# pip cache so they don't fail mid-build with "No space left" (#1219)
|
# pip cache so they don't fail mid-build with "No space left" (#1219)
|
||||||
@@ -908,6 +922,8 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
ps_lines = []
|
ps_lines = []
|
||||||
ps_lines.append('$sessionDir = "$env:TEMP\\odysseus-sessions"')
|
ps_lines.append('$sessionDir = "$env:TEMP\\odysseus-sessions"')
|
||||||
ps_lines.append('New-Item -ItemType Directory -Force -Path $sessionDir | Out-Null')
|
ps_lines.append('New-Item -ItemType Directory -Force -Path $sessionDir | Out-Null')
|
||||||
|
ps_lines.append('$env:PYTHONIOENCODING = "utf-8"')
|
||||||
|
ps_lines.append('$env:PYTHONUTF8 = "1"')
|
||||||
if req.hf_token:
|
if req.hf_token:
|
||||||
ps_lines.append(f"$env:HF_TOKEN = '{_ps_squote(req.hf_token)}'")
|
ps_lines.append(f"$env:HF_TOKEN = '{_ps_squote(req.hf_token)}'")
|
||||||
if req.gpus:
|
if req.gpus:
|
||||||
@@ -926,7 +942,7 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
ps_lines.append('try { python -c "import llama_cpp" 2>$null } catch {}')
|
ps_lines.append('try { python -c "import llama_cpp" 2>$null } catch {}')
|
||||||
ps_lines.append('if ($LASTEXITCODE -ne 0) {')
|
ps_lines.append('if ($LASTEXITCODE -ne 0) {')
|
||||||
ps_lines.append(' Write-Host "Installing llama-cpp-python..."')
|
ps_lines.append(' Write-Host "Installing llama-cpp-python..."')
|
||||||
ps_lines.append(' python -m pip install llama-cpp-python[server]')
|
ps_lines.append(' python -m pip install llama-cpp-python[server] --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu')
|
||||||
ps_lines.append('}')
|
ps_lines.append('}')
|
||||||
elif "vllm" in req.cmd:
|
elif "vllm" in req.cmd:
|
||||||
ps_lines.append('Write-Host "ERROR: vLLM is not supported on Windows. Use Ollama or llama.cpp instead."')
|
ps_lines.append('Write-Host "ERROR: vLLM is not supported on Windows. Use Ollama or llama.cpp instead."')
|
||||||
@@ -1001,45 +1017,57 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
# ollama is found (otherwise macOS falls back to a slow source build).
|
# ollama is found (otherwise macOS falls back to a slow source build).
|
||||||
# /opt/homebrew = Apple Silicon, /usr/local = Intel; harmless on Linux.
|
# /opt/homebrew = Apple Silicon, /usr/local = Intel; harmless on Linux.
|
||||||
runner_lines.append('export PATH="$HOME/.local/bin:$HOME/bin:$HOME/llama.cpp/build/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"')
|
runner_lines.append('export PATH="$HOME/.local/bin:$HOME/bin:$HOME/llama.cpp/build/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"')
|
||||||
runner_lines.append('if [ -d /data/data/com.termux ]; then')
|
if local_windows:
|
||||||
runner_lines.append(' # Termux: no native build — use the Python bindings (CPU).')
|
# LOCAL Windows: no native source compilation (no cmake/compiler on Git Bash).
|
||||||
runner_lines.append(' if ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
# Just check python bindings (using native `python` binary) and fall back to pip install.
|
||||||
runner_lines.append(' pkg install -y cmake 2>/dev/null')
|
runner_lines.append('if ! command -v llama-server &>/dev/null && ! python -c "import llama_cpp" 2>/dev/null; then')
|
||||||
runner_lines.append(' pip install numpy diskcache jinja2 2>/dev/null')
|
runner_lines.append(' echo "llama-server not found — installing Python bindings..."')
|
||||||
runner_lines.append(' CMAKE_ARGS="-DGGML_BLAS=OFF -DGGML_LLAMAFILE=OFF" pip install \'llama-cpp-python[server]\' --no-build-isolation --no-cache-dir 2>&1 || true')
|
runner_lines.append(f" {_pip_install_fallback_chain('llama-cpp-python[server]', python_cmd='python')} || true")
|
||||||
runner_lines.append(' fi')
|
runner_lines.append('fi')
|
||||||
runner_lines.append('elif ! command -v llama-server &>/dev/null; then')
|
runner_lines.append('if ! command -v llama-server &>/dev/null && ! python -c "import llama_cpp" 2>/dev/null; then')
|
||||||
runner_lines.append(' echo "Native llama-server not found — building from source (one-time, may take a few minutes)..."')
|
runner_lines.append(' echo "ERROR: llama.cpp serving is not available after install attempts."')
|
||||||
runner_lines.append(' mkdir -p ~/bin')
|
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||||
runner_lines.append(' cd ~ && [ -d llama.cpp ] || git clone --depth 1 https://github.com/ggml-org/llama.cpp')
|
runner_lines.append('fi')
|
||||||
# Build with the right accelerator: Metal on macOS (llama.cpp
|
else:
|
||||||
# enables it automatically, no flag), CUDA on Linux when present,
|
runner_lines.append('if [ -d /data/data/com.termux ]; then')
|
||||||
# else a plain CPU build. nproc is Linux-only — fall back to
|
runner_lines.append(' # Termux: no native build — use the Python bindings (CPU).')
|
||||||
# `sysctl hw.ncpu` on macOS. (Tip: `brew install llama.cpp` ships
|
runner_lines.append(' if ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
||||||
# a prebuilt llama-server and skips this whole source build.)
|
runner_lines.append(' pkg install -y cmake 2>/dev/null')
|
||||||
runner_lines.append(' NPROC="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"')
|
runner_lines.append(' pip install numpy diskcache jinja2 2>/dev/null')
|
||||||
runner_lines.append(' if [ "$(uname -s)" = "Darwin" ]; then')
|
runner_lines.append(' CMAKE_ARGS="-DGGML_BLAS=OFF -DGGML_LLAMAFILE=OFF" pip install \'llama-cpp-python[server]\' --no-build-isolation --no-cache-dir 2>&1 || true')
|
||||||
runner_lines.append(' command -v cmake >/dev/null 2>&1 || echo "WARNING: cmake not found — install it with: brew install cmake (or: brew install llama.cpp for a prebuilt llama-server)."')
|
runner_lines.append(' fi')
|
||||||
# Start from a clean cache: a prior failed configure (e.g. a CUDA
|
runner_lines.append('elif ! command -v llama-server &>/dev/null; then')
|
||||||
# attempt) poisons build/CMakeCache.txt, so a plain `cmake -B build`
|
runner_lines.append(' echo "Native llama-server not found — building from source (one-time, may take a few minutes)..."')
|
||||||
# would reuse the bad settings and fail again. CMAKE_BUILD_TYPE is
|
runner_lines.append(' mkdir -p ~/bin')
|
||||||
# explicit so the binary is optimized (Metal auto-enables on macOS).
|
runner_lines.append(' cd ~ && [ -d llama.cpp ] || git clone --depth 1 https://github.com/ggml-org/llama.cpp')
|
||||||
runner_lines.append(' cd ~/llama.cpp && rm -rf build && cmake -B build -DCMAKE_BUILD_TYPE=Release \\')
|
# Build with the right accelerator: Metal on macOS (llama.cpp
|
||||||
runner_lines.append(' && cmake --build build -j"$NPROC" --target llama-server \\')
|
# enables it automatically, no flag), CUDA on Linux when present,
|
||||||
runner_lines.append(' && ln -sf ~/llama.cpp/build/bin/llama-server ~/bin/llama-server')
|
# else a plain CPU build. nproc is Linux-only — fall back to
|
||||||
runner_lines.append(' else')
|
# `sysctl hw.ncpu` on macOS. (Tip: `brew install llama.cpp` ships
|
||||||
_append_llama_cpp_linux_accel_build_lines(runner_lines)
|
# a prebuilt llama-server and skips this whole source build.)
|
||||||
runner_lines.append(' fi')
|
runner_lines.append(' NPROC="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"')
|
||||||
runner_lines.append(' # If the native build failed, fall back to the Python bindings.')
|
runner_lines.append(' if [ "$(uname -s)" = "Darwin" ]; then')
|
||||||
runner_lines.append(' if ! command -v llama-server &>/dev/null && ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
runner_lines.append(' command -v cmake >/dev/null 2>&1 || echo "WARNING: cmake not found — install it with: brew install cmake (or: brew install llama.cpp for a prebuilt llama-server)."')
|
||||||
runner_lines.append(' echo "llama-server build failed — installing Python bindings as fallback..."')
|
# Start from a clean cache: a prior failed configure (e.g. a CUDA
|
||||||
runner_lines.append(f" {_pip_install_fallback_chain('llama-cpp-python[server]', python_cmd='pip')} || true")
|
# attempt) poisons build/CMakeCache.txt, so a plain `cmake -B build`
|
||||||
runner_lines.append(' fi')
|
# would reuse the bad settings and fail again. CMAKE_BUILD_TYPE is
|
||||||
runner_lines.append(' if ! command -v llama-server &>/dev/null && ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
# explicit so the binary is optimized (Metal auto-enables on macOS).
|
||||||
runner_lines.append(' echo "ERROR: llama.cpp serving is not available after install/build attempts."')
|
runner_lines.append(' cd ~/llama.cpp && rm -rf build && cmake -B build -DCMAKE_BUILD_TYPE=Release \\')
|
||||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
runner_lines.append(' && cmake --build build -j"$NPROC" --target llama-server \\')
|
||||||
runner_lines.append(' fi')
|
runner_lines.append(' && ln -sf ~/llama.cpp/build/bin/llama-server ~/bin/llama-server')
|
||||||
runner_lines.append('fi')
|
runner_lines.append(' else')
|
||||||
|
_append_llama_cpp_linux_accel_build_lines(runner_lines)
|
||||||
|
runner_lines.append(' fi')
|
||||||
|
# If the native build failed, fall back to the Python bindings.
|
||||||
|
runner_lines.append(' if ! command -v llama-server &>/dev/null && ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
||||||
|
runner_lines.append(' echo "llama-server build failed — installing Python bindings as fallback..."')
|
||||||
|
runner_lines.append(f" {_pip_install_fallback_chain('llama-cpp-python[server]', python_cmd='pip')} || true")
|
||||||
|
runner_lines.append(' fi')
|
||||||
|
runner_lines.append(' if ! command -v llama-server &>/dev/null && ! python3 -c "import llama_cpp" 2>/dev/null; then')
|
||||||
|
runner_lines.append(' echo "ERROR: llama.cpp serving is not available after install/build attempts."')
|
||||||
|
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||||
|
runner_lines.append(' fi')
|
||||||
|
runner_lines.append('fi')
|
||||||
elif "ollama" in req.cmd:
|
elif "ollama" in req.cmd:
|
||||||
handled_ollama_serve = True
|
handled_ollama_serve = True
|
||||||
_ollama_default_host = "0.0.0.0" if remote else "127.0.0.1"
|
_ollama_default_host = "0.0.0.0" if remote else "127.0.0.1"
|
||||||
@@ -2076,7 +2104,11 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
"inc=os.path.isdir(blobs) and any(x.endswith('.incomplete') for x in os.listdir(blobs));"
|
"inc=os.path.isdir(blobs) and any(x.endswith('.incomplete') for x in os.listdir(blobs));"
|
||||||
"sys.exit(0 if ok and not inc else 1)"
|
"sys.exit(0 if ok and not inc else 1)"
|
||||||
)
|
)
|
||||||
cmd = ["python3", "-c", py, repo_id]
|
if not remote_host:
|
||||||
|
import sys
|
||||||
|
cmd = [sys.executable, "-c", py, repo_id]
|
||||||
|
else:
|
||||||
|
cmd = ["python3", "-c", py, repo_id]
|
||||||
try:
|
try:
|
||||||
if remote_host:
|
if remote_host:
|
||||||
ssh_base = ["ssh"]
|
ssh_base = ["ssh"]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from core.platform_compat import (
|
|||||||
IS_WINDOWS,
|
IS_WINDOWS,
|
||||||
detached_popen_kwargs,
|
detached_popen_kwargs,
|
||||||
find_bash,
|
find_bash,
|
||||||
|
git_bash_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -368,8 +369,12 @@ async def _create_shell(command: str, **kwargs):
|
|||||||
POSIX: /bin/sh via create_subprocess_shell (unchanged behaviour).
|
POSIX: /bin/sh via create_subprocess_shell (unchanged behaviour).
|
||||||
Windows: prefer a real bash (Git Bash/WSL) so bash-syntax commands behave
|
Windows: prefer a real bash (Git Bash/WSL) so bash-syntax commands behave
|
||||||
the same as on Linux; fall back to cmd.exe when no bash is installed.
|
the same as on Linux; fall back to cmd.exe when no bash is installed.
|
||||||
|
Powershell commands are executed directly via cmd.exe /c to avoid quoting
|
||||||
|
and env variable expansion errors under Git Bash.
|
||||||
"""
|
"""
|
||||||
if IS_WINDOWS:
|
if IS_WINDOWS:
|
||||||
|
if command.strip().lower().startswith("powershell"):
|
||||||
|
return await asyncio.create_subprocess_shell(command, **kwargs)
|
||||||
bash = find_bash()
|
bash = find_bash()
|
||||||
if bash:
|
if bash:
|
||||||
return await asyncio.create_subprocess_exec(bash, "-c", command, **kwargs)
|
return await asyncio.create_subprocess_exec(bash, "-c", command, **kwargs)
|
||||||
@@ -672,8 +677,8 @@ async def _generate_win_detached(cmd: str, request: Request):
|
|||||||
if bash:
|
if bash:
|
||||||
script_path = TMUX_LOG_DIR / f"{session_id}.sh"
|
script_path = TMUX_LOG_DIR / f"{session_id}.sh"
|
||||||
script_path.write_text(
|
script_path.write_text(
|
||||||
f"{cmd} > {shlex.quote(str(log_path))} 2>&1\n"
|
f"{cmd} > {shlex.quote(git_bash_path(log_path))} 2>&1\n"
|
||||||
f"echo $? > {shlex.quote(str(exit_path))}\n",
|
f"echo $? > {shlex.quote(git_bash_path(exit_path))}\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
argv = [bash, str(script_path)]
|
argv = [bash, str(script_path)]
|
||||||
|
|||||||
+2
-1
@@ -33,6 +33,7 @@ from core.atomic_io import atomic_write_json
|
|||||||
from core.platform_compat import (
|
from core.platform_compat import (
|
||||||
detached_popen_kwargs,
|
detached_popen_kwargs,
|
||||||
find_bash,
|
find_bash,
|
||||||
|
git_bash_path,
|
||||||
kill_process_tree,
|
kill_process_tree,
|
||||||
pid_alive,
|
pid_alive,
|
||||||
)
|
)
|
||||||
@@ -106,7 +107,7 @@ def launch(command: str, session_id: str, cwd: Optional[str] = None,
|
|||||||
# handles drive paths and spaces correctly.
|
# handles drive paths and spaces correctly.
|
||||||
cmd_path = _JOBS_DIR / f"{job_id}.cmd.sh"
|
cmd_path = _JOBS_DIR / f"{job_id}.cmd.sh"
|
||||||
cmd_path.write_text(command + "\n", encoding="utf-8")
|
cmd_path.write_text(command + "\n", encoding="utf-8")
|
||||||
lp, xp, cp = (shlex.quote(p.as_posix()) for p in (log_path, exit_path, cmd_path))
|
lp, xp, cp = (shlex.quote(git_bash_path(p)) for p in (log_path, exit_path, cmd_path))
|
||||||
script_path = _JOBS_DIR / f"{job_id}.sh"
|
script_path = _JOBS_DIR / f"{job_id}.sh"
|
||||||
script_path.write_text(
|
script_path.write_text(
|
||||||
f"bash {cp} > {lp} 2>&1\n"
|
f"bash {cp} > {lp} 2>&1\n"
|
||||||
|
|||||||
@@ -426,6 +426,15 @@ export const ERROR_PATTERNS = [
|
|||||||
{ label: 'Copy install command', action: () => _copyText('pip install "llama-cpp-python[server]"') },
|
{ label: 'Copy install command', action: () => _copyText('pip install "llama-cpp-python[server]"') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /Windows Error 0xc000001d|Illegal instruction|0xc000001d/i,
|
||||||
|
message: 'AVX2 Instruction Set Mismatch: the precompiled llama-cpp-python wheel requires CPU features (AVX2/FMA) that your processor or virtual machine lacks.',
|
||||||
|
suggestion: 'Suggested action: switch this serve config to Ollama (highly recommended, has dynamic CPU fallbacks), or choose a remote Linux GPU server.',
|
||||||
|
fixes: [
|
||||||
|
{ label: 'Switch to Ollama', action: (panel) => _openServeEditFromDiagnosis(panel, { backend: 'ollama' }) },
|
||||||
|
{ label: 'Choose remote server', action: (panel) => _openServeEditFromDiagnosis(panel) },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /CUDA Toolkit not found|Unable to find cudart library|missing:\s*CUDA_CUDART/i,
|
pattern: /CUDA Toolkit not found|Unable to find cudart library|missing:\s*CUDA_CUDART/i,
|
||||||
message: 'llama.cpp found nvcc, but the CUDA runtime library is missing.',
|
message: 'llama.cpp found nvcc, but the CUDA runtime library is missing.',
|
||||||
|
|||||||
+12
-3
@@ -161,8 +161,17 @@ function _getPort(hostOrTask) {
|
|||||||
|
|
||||||
/** Get platform for a given host (or task object). Returns 'windows', 'termux', 'linux', or '' */
|
/** Get platform for a given host (or task object). Returns 'windows', 'termux', 'linux', or '' */
|
||||||
export function _getPlatform(hostOrTask) {
|
export function _getPlatform(hostOrTask) {
|
||||||
if (!hostOrTask) return _envState.platform || '';
|
const isWinBrowser = (window.navigator.userAgent || window.navigator.platform || '').toLowerCase().includes('win');
|
||||||
if (typeof hostOrTask === 'object') return hostOrTask.platform || _getPlatform(hostOrTask.remoteHost);
|
if (!hostOrTask || hostOrTask === 'local') {
|
||||||
|
return _envState.platform || (isWinBrowser ? 'windows' : '');
|
||||||
|
}
|
||||||
|
if (typeof hostOrTask === 'object') {
|
||||||
|
const h = hostOrTask.remoteHost;
|
||||||
|
if (!h || h === 'local') {
|
||||||
|
return hostOrTask.platform || _envState.platform || (isWinBrowser ? 'windows' : '');
|
||||||
|
}
|
||||||
|
return hostOrTask.platform || _getPlatform(h);
|
||||||
|
}
|
||||||
const srv = _envState.servers.find(s => s.host === hostOrTask);
|
const srv = _envState.servers.find(s => s.host === hostOrTask);
|
||||||
return srv?.platform || '';
|
return srv?.platform || '';
|
||||||
}
|
}
|
||||||
@@ -637,7 +646,7 @@ async function _fetchDependencies() {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const pkgs = data.packages || [];
|
const pkgs = data.packages || [];
|
||||||
if (!pkgs.length) { list.innerHTML = '<div class="hwfit-loading">No packages found</div>'; return; }
|
if (!pkgs.length) { list.innerHTML = '<div class="hwfit-loading">No packages found</div>'; return; }
|
||||||
const _winUnsupported = new Set(['diffusers', 'hf_transfer', 'vllm', 'rembg', 'gfpgan']);
|
const _winUnsupported = new Set(['vllm', 'rembg', 'gfpgan']);
|
||||||
|
|
||||||
const _statusTag = (pkg, isLocal, isSystemDep, winBlocked) => {
|
const _statusTag = (pkg, isLocal, isSystemDep, winBlocked) => {
|
||||||
if (winBlocked) return `<span class="cookbook-dep-tag cookbook-dep-na">N/A</span>`;
|
if (winBlocked) return `<span class="cookbook-dep-tag cookbook-dep-na">N/A</span>`;
|
||||||
|
|||||||
@@ -2738,6 +2738,7 @@ async function _reconnectTask(el, task) {
|
|||||||
_updateTask(task.sessionId, { status: 'done', _doneConfirmAt: null, _lastStatusFlipAt: Date.now() });
|
_updateTask(task.sessionId, { status: 'done', _doneConfirmAt: null, _lastStatusFlipAt: Date.now() });
|
||||||
const _el = document.querySelector(`.cookbook-task[data-task-id="${task.sessionId}"]`);
|
const _el = document.querySelector(`.cookbook-task[data-task-id="${task.sessionId}"]`);
|
||||||
if (_el) {
|
if (_el) {
|
||||||
|
_clearDiagnosis(_el);
|
||||||
_el.dataset.status = 'done';
|
_el.dataset.status = 'done';
|
||||||
const _badge = _el.querySelector('.cookbook-task-status');
|
const _badge = _el.querySelector('.cookbook-task-status');
|
||||||
if (_badge) { _badge.textContent = _statusLabel('done', task.type); _badge.className = 'cookbook-task-status cookbook-task-done'; }
|
if (_badge) { _badge.textContent = _statusLabel('done', task.type); _badge.className = 'cookbook-task-status cookbook-task-done'; }
|
||||||
@@ -2804,13 +2805,14 @@ async function _reconnectTask(el, task) {
|
|||||||
const curProgress = computeProgressSignal(_bytes, _dlAgg, lastPct, snapshot);
|
const curProgress = computeProgressSignal(_bytes, _dlAgg, lastPct, snapshot);
|
||||||
const _fetchPctMatches = [...snapshot.matchAll(/Fetching\s+\d+\s+files:\s*(\d+)%/g)];
|
const _fetchPctMatches = [...snapshot.matchAll(/Fetching\s+\d+\s+files:\s*(\d+)%/g)];
|
||||||
const _fetchPct = _fetchPctMatches.length ? parseInt(_fetchPctMatches[_fetchPctMatches.length - 1][1]) : null;
|
const _fetchPct = _fetchPctMatches.length ? parseInt(_fetchPctMatches[_fetchPctMatches.length - 1][1]) : null;
|
||||||
|
const isPipDep = !!(task.payload && task.payload._dep);
|
||||||
const _startupStalled = !_bytes && ((_dlAgg === 0) || (_fetchPct === 0)) && curProgress === '0';
|
const _startupStalled = !_bytes && ((_dlAgg === 0) || (_fetchPct === 0)) && curProgress === '0';
|
||||||
const _STALE_TIMEOUT = _startupStalled ? STARTUP_STALE_PROGRESS_MS : STALE_PROGRESS_MS;
|
const _STALE_TIMEOUT = _startupStalled ? STARTUP_STALE_PROGRESS_MS : STALE_PROGRESS_MS;
|
||||||
if (!el._lastProgress) { el._lastProgress = curProgress; el._lastProgressTime = Date.now(); }
|
if (!el._lastProgress) { el._lastProgress = curProgress; el._lastProgressTime = Date.now(); }
|
||||||
if (curProgress !== el._lastProgress) {
|
if (curProgress !== el._lastProgress) {
|
||||||
el._lastProgress = curProgress;
|
el._lastProgress = curProgress;
|
||||||
el._lastProgressTime = Date.now();
|
el._lastProgressTime = Date.now();
|
||||||
} else if (Date.now() - (el._lastProgressTime || 0) > _STALE_TIMEOUT && task._autoRestarted) {
|
} else if (!isPipDep && Date.now() - (el._lastProgressTime || 0) > _STALE_TIMEOUT && task._autoRestarted) {
|
||||||
const mins = Math.floor((Date.now() - (el._lastProgressTime || 0)) / 60000);
|
const mins = Math.floor((Date.now() - (el._lastProgressTime || 0)) / 60000);
|
||||||
// Already auto-restarted once and stalled again — make the badge a
|
// Already auto-restarted once and stalled again — make the badge a
|
||||||
// one-click retry (resumes from the cached partial files) so the
|
// one-click retry (resumes from the cached partial files) so the
|
||||||
@@ -2823,7 +2825,7 @@ async function _reconnectTask(el, task) {
|
|||||||
badge._retryBound = true;
|
badge._retryBound = true;
|
||||||
badge.addEventListener('click', (e) => { e.stopPropagation(); _retryTask(el, task); });
|
badge.addEventListener('click', (e) => { e.stopPropagation(); _retryTask(el, task); });
|
||||||
}
|
}
|
||||||
} else if (Date.now() - (el._lastProgressTime || 0) > _STALE_TIMEOUT && !task._autoRestarted) {
|
} else if (!isPipDep && Date.now() - (el._lastProgressTime || 0) > _STALE_TIMEOUT && !task._autoRestarted) {
|
||||||
task._autoRestarted = true;
|
task._autoRestarted = true;
|
||||||
_updateTask(task.sessionId, { _autoRestarted: true });
|
_updateTask(task.sessionId, { _autoRestarted: true });
|
||||||
badge.textContent = _startupStalled ? '0% stall — retrying' : 'stale — restarting';
|
badge.textContent = _startupStalled ? '0% stall — retrying' : 'stale — restarting';
|
||||||
@@ -2975,6 +2977,7 @@ async function _reconnectTask(el, task) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (snapshot.includes('DOWNLOAD_OK') || (snapshot.includes('/snapshots/') && completed >= totalFiles && totalFiles > 0)) {
|
if (snapshot.includes('DOWNLOAD_OK') || (snapshot.includes('/snapshots/') && completed >= totalFiles && totalFiles > 0)) {
|
||||||
|
_clearDiagnosis(el);
|
||||||
_dlRetryCount.delete(task.payload?.repo_id || task.name);
|
_dlRetryCount.delete(task.payload?.repo_id || task.name);
|
||||||
badge.textContent = _statusLabel('done', task.type);
|
badge.textContent = _statusLabel('done', task.type);
|
||||||
badge.className = 'cookbook-task-status cookbook-task-done';
|
badge.className = 'cookbook-task-status cookbook-task-done';
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import sys
|
||||||
|
for mod_name in ["src.endpoint_resolver", "src.database", "core.database"]:
|
||||||
|
_mod = sys.modules.get(mod_name)
|
||||||
|
if _mod is not None and not getattr(_mod, "__file__", None):
|
||||||
|
sys.modules.pop(mod_name, None)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ def test_pip_install_attempt_failure_propagates_real_exit_code():
|
|||||||
"""Run the generated snippet against a deliberately broken pip install
|
"""Run the generated snippet against a deliberately broken pip install
|
||||||
to confirm the subshell exits with pip's non-zero status."""
|
to confirm the subshell exits with pip's non-zero status."""
|
||||||
snippet = _pip_install_attempt("python3 -m pip install __nonexistent_package_12345__")
|
snippet = _pip_install_attempt("python3 -m pip install __nonexistent_package_12345__")
|
||||||
|
if sys.platform == "win32":
|
||||||
|
snippet = snippet.replace("$", "\\$")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "-c", snippet],
|
["bash", "-c", snippet],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -250,6 +252,8 @@ def test_pip_install_attempt_failure_propagates_real_exit_code():
|
|||||||
def test_pip_install_attempt_success_exits_zero():
|
def test_pip_install_attempt_success_exits_zero():
|
||||||
"""When pip succeeds, the subshell should exit 0."""
|
"""When pip succeeds, the subshell should exit 0."""
|
||||||
snippet = _pip_install_attempt("python3 -c 'pass'")
|
snippet = _pip_install_attempt("python3 -c 'pass'")
|
||||||
|
if sys.platform == "win32":
|
||||||
|
snippet = snippet.replace("$", "\\$")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "-c", snippet],
|
["bash", "-c", snippet],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -262,6 +266,8 @@ def test_pip_install_attempt_success_exits_zero():
|
|||||||
def test_pip_install_attempt_surfaces_stderr_on_failure():
|
def test_pip_install_attempt_surfaces_stderr_on_failure():
|
||||||
"""On failure, the last 5 lines of pip output should appear in stdout."""
|
"""On failure, the last 5 lines of pip output should appear in stdout."""
|
||||||
snippet = _pip_install_attempt("python3 -m pip install __nonexistent_package_12345__")
|
snippet = _pip_install_attempt("python3 -m pip install __nonexistent_package_12345__")
|
||||||
|
if sys.platform == "win32":
|
||||||
|
snippet = snippet.replace("$", "\\$")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "-c", snippet],
|
["bash", "-c", snippet],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -354,6 +360,15 @@ def test_validate_serve_cmd_accepts_llama_advanced_controls():
|
|||||||
assert _validate_serve_cmd(cmd) == cmd
|
assert _validate_serve_cmd(cmd) == cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_serve_cmd_accepts_windows_printf_format():
|
||||||
|
cmd = (
|
||||||
|
"python -m llama_cpp.server --model "
|
||||||
|
"\"$(printf %s ${HOME}'/.cache/huggingface/hub/models--unsloth--Qwen3.5-2B-GGUF/snapshots/f6d5376be1edb4d416d56da11e5397a961aca8ae/Qwen3.5-2B-Q4_K_M.gguf')\" "
|
||||||
|
"--host 0.0.0.0 --port 8000 --n_gpu_layers 99 --n_ctx 32768 --flash_attn true --type_k q4_0 --type_v q4_0"
|
||||||
|
)
|
||||||
|
assert _validate_serve_cmd(cmd) == cmd
|
||||||
|
|
||||||
|
|
||||||
def test_ollama_serve_defaults_to_loopback_bind():
|
def test_ollama_serve_defaults_to_loopback_bind():
|
||||||
assert _ollama_bind_from_cmd("ollama serve") == ("127.0.0.1", "11434")
|
assert _ollama_bind_from_cmd("ollama serve") == ("127.0.0.1", "11434")
|
||||||
assert _ollama_bind_from_cmd("ollama run qwen2.5:0.5b") == ("127.0.0.1", "11434")
|
assert _ollama_bind_from_cmd("ollama run qwen2.5:0.5b") == ("127.0.0.1", "11434")
|
||||||
@@ -481,11 +496,13 @@ def test_llama_cpp_rebuild_cmd_clears_cached_build_paths():
|
|||||||
def test_llama_cpp_rebuild_cmd_runs_clean_on_a_fresh_home(tmp_path):
|
def test_llama_cpp_rebuild_cmd_runs_clean_on_a_fresh_home(tmp_path):
|
||||||
"""The command should succeed even when neither path exists yet."""
|
"""The command should succeed even when neither path exists yet."""
|
||||||
import os
|
import os
|
||||||
|
from core.platform_compat import find_bash, git_bash_path
|
||||||
|
|
||||||
|
bash = find_bash() or "bash"
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
env["HOME"] = str(tmp_path)
|
env["HOME"] = git_bash_path(tmp_path)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "-c", _llama_cpp_rebuild_cmd()],
|
[bash, "-c", _llama_cpp_rebuild_cmd()],
|
||||||
capture_output=True, text=True, env=env, timeout=10,
|
capture_output=True, text=True, env=env, timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user