mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
fix(cookbook): harden remote serve host handling (#4345)
This commit is contained in:
@@ -46,8 +46,12 @@ def _ssh_prefix_for_task(task: dict) -> tuple[str, str]:
|
||||
shell metacharacters in ``remoteHost`` is rejected with 400 rather than
|
||||
injected.
|
||||
"""
|
||||
host = validate_remote_host((task.get("remoteHost") or "").strip() or None) or ""
|
||||
ssh_port = validate_ssh_port((task.get("sshPort") or "").strip() or None) or ""
|
||||
raw_host = task.get("remoteHost")
|
||||
raw_port = task.get("sshPort")
|
||||
host_value = str(raw_host).strip() if raw_host is not None else None
|
||||
port_value = str(raw_port).strip() if raw_port is not None else None
|
||||
host = validate_remote_host(host_value or None) or ""
|
||||
ssh_port = validate_ssh_port(port_value or None) or ""
|
||||
port_flag = f"-p {ssh_port} " if ssh_port and ssh_port != "22" else ""
|
||||
return host, port_flag
|
||||
|
||||
|
||||
@@ -1284,6 +1284,11 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# LOCAL execution on a native-Windows host never uses tmux (detached
|
||||
# process path below), regardless of the UI-supplied platform.
|
||||
local_windows = IS_WINDOWS and not remote
|
||||
if is_windows and remote and "diffusion_server.py" in req.cmd:
|
||||
raise HTTPException(
|
||||
400,
|
||||
"Remote Windows Diffusers serving is not supported yet; use local Windows or a Linux remote server.",
|
||||
)
|
||||
|
||||
if not is_windows and not local_windows and not await _binary_available("tmux", remote, req.ssh_port):
|
||||
return {
|
||||
|
||||
+34
-31
@@ -116,13 +116,28 @@ function _selectedServeTarget(panel) {
|
||||
: (server?.name || 'local server');
|
||||
return {
|
||||
host,
|
||||
port: host ? (_getPort(host) || server?.port || '') : '',
|
||||
port: host ? (server?.port || _getPort(host) || '') : '',
|
||||
env: server?.env || '',
|
||||
venv,
|
||||
platform: server?.platform || _envState.platform || '',
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
function _remoteWindowsDiffusersUnsupported(target) {
|
||||
return !!(target?.host && target?.platform === 'windows');
|
||||
}
|
||||
|
||||
function _backendChoicesForTarget(target) {
|
||||
if (target?.platform === 'windows') {
|
||||
if (_remoteWindowsDiffusersUnsupported(target)) return [['llamacpp','llama.cpp']];
|
||||
return [['llamacpp','llama.cpp'],['diffusers','Diffusers']];
|
||||
}
|
||||
return _isMetal()
|
||||
? [['llamacpp','llama.cpp'],['ollama','Ollama']]
|
||||
: [['vllm','vLLM'],['sglang','SGLang'],['llamacpp','llama.cpp'],['ollama','Ollama'],['diffusers','Diffusers']];
|
||||
}
|
||||
|
||||
async function _fetchServeRuntimePackage(panel, backend) {
|
||||
const packageByBackend = {
|
||||
vllm: 'vllm',
|
||||
@@ -529,13 +544,14 @@ function _rerenderCachedModels() {
|
||||
const ss = (_byRepo[repo] && typeof _byRepo[repo] === 'object')
|
||||
? _byRepo[repo]
|
||||
: (_lastUsed || (_isLegacyFlat ? _allSs : {}));
|
||||
const _serveTarget = _selectedServeTarget();
|
||||
const _backendChoices = _backendChoicesForTarget(_serveTarget);
|
||||
const _allowedBackends = new Set(_backendChoices.map(([v]) => v));
|
||||
const detectedBackend = _detectBackend(m).backend;
|
||||
const _allowedBackends = new Set(_isWindows()
|
||||
? ['llamacpp', 'diffusers']
|
||||
: (_isMetal() ? ['llamacpp', 'ollama'] : ['vllm', 'sglang', 'llamacpp', 'ollama', 'diffusers']));
|
||||
const defaultBackend = (ss._forceBackend && ss.backend && _allowedBackends.has(ss.backend))
|
||||
let defaultBackend = (ss._forceBackend && ss.backend && _allowedBackends.has(ss.backend))
|
||||
? ss.backend
|
||||
: detectedBackend;
|
||||
if (!_allowedBackends.has(defaultBackend)) defaultBackend = _backendChoices[0]?.[0] || detectedBackend;
|
||||
const savedMatchesBackend = !!ss._forceBackend || (ss.backend || 'vllm') === detectedBackend;
|
||||
const sv = (k, def) => (ss[k] !== undefined && savedMatchesBackend) ? ss[k] : def;
|
||||
const defaultTp = defaultBackend === 'llamacpp' ? '1' : sv('tp', '1');
|
||||
@@ -607,12 +623,6 @@ function _rerenderCachedModels() {
|
||||
}
|
||||
// Row 1: Backend + Server + Env
|
||||
panelHtml += `<div class="hwfit-serve-row">`;
|
||||
const _backendChoices = _isWindows()
|
||||
? [['llamacpp','llama.cpp'],['diffusers','Diffusers']]
|
||||
: _isMetal()
|
||||
// Diffusers (diffusion_server.py) is CUDA-only — omit it on Metal.
|
||||
? [['llamacpp','llama.cpp'],['ollama','Ollama']]
|
||||
: [['vllm','vLLM'],['sglang','SGLang'],['llamacpp','llama.cpp'],['ollama','Ollama'],['diffusers','Diffusers']];
|
||||
const backendOpts = _backendChoices.map(([v,l]) => `<option value="${v}"${defaultBackend===v?' selected':''}>${l}</option>`).join('');
|
||||
// Custom Backend picker — native <select> can't host SVG inside
|
||||
// options, so we render a button + menu that show the backend logo
|
||||
@@ -1971,6 +1981,12 @@ function _rerenderCachedModels() {
|
||||
else serveState[el.dataset.field] = el.value;
|
||||
});
|
||||
serveState.backend = serveState.backend || (_detectBackend(m).backend) || 'vllm';
|
||||
const launchTarget = _selectedServeTarget(panel);
|
||||
if (serveState.backend === 'diffusers' && _remoteWindowsDiffusersUnsupported(launchTarget)) {
|
||||
_restoreLaunchBtn();
|
||||
uiModule.showToast('Diffusers serving is not supported on remote Windows servers yet. Use local Windows or a Linux server.', 9000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-launch: check our own task list for a serve already running
|
||||
// on this host. Offer to stop+launch as the default action — the
|
||||
@@ -1979,7 +1995,7 @@ function _rerenderCachedModels() {
|
||||
// common case instantly without waiting for a network round-trip.
|
||||
try {
|
||||
const _runningMod = await import('./cookbookRunning.js');
|
||||
const _hostStr = _envState.remoteHost || '';
|
||||
const _hostStr = launchTarget.host || '';
|
||||
const _active = (_runningMod._loadTasks ? _runningMod._loadTasks() : []).filter(t =>
|
||||
t && t.type === 'serve'
|
||||
&& (t.remoteHost || '') === _hostStr
|
||||
@@ -2033,12 +2049,11 @@ function _rerenderCachedModels() {
|
||||
|| (serveState.backend === 'diffusers');
|
||||
if (_needsGpu) {
|
||||
try {
|
||||
const _probeHost = (_envState.remoteHost || '').trim();
|
||||
const _probeHost = (launchTarget.host || '').trim();
|
||||
const _probeParams = new URLSearchParams();
|
||||
if (_probeHost) {
|
||||
_probeParams.set('host', _probeHost);
|
||||
const _sp = (_serverByVal?.(_envState.remoteServerKey || _probeHost) || {}).port;
|
||||
if (_sp) _probeParams.set('ssh_port', _sp);
|
||||
if (launchTarget.port) _probeParams.set('ssh_port', launchTarget.port);
|
||||
}
|
||||
const _probeRes = await fetch('/api/cookbook/gpus' + (_probeParams.toString() ? '?' + _probeParams : ''), { credentials: 'same-origin' });
|
||||
const _probeData = await _probeRes.json();
|
||||
@@ -2071,10 +2086,10 @@ function _rerenderCachedModels() {
|
||||
|| launchCmd.match(/OLLAMA_HOST=[^:\s]+:(\d{2,5})\b/);
|
||||
const _port = _portMatch ? _portMatch[1] : '';
|
||||
if (_port) {
|
||||
const _portHost = (_envState.remoteHost || '').trim();
|
||||
const _portHost = (launchTarget.host || '').trim();
|
||||
const _checkInner = `ss -tlnp 2>/dev/null | awk '$4 ~ /:${_port}$/ {print; exit}' || netstat -tlnp 2>/dev/null | awk '$4 ~ /:${_port}$/ {print; exit}'`;
|
||||
const _cmd = _portHost
|
||||
? `ss h ${_portHost} <<<"" 2>/dev/null; ssh -o ConnectTimeout=4 -o StrictHostKeyChecking=no ${_portHost} ${JSON.stringify(_checkInner)}`
|
||||
? `ssh -o ConnectTimeout=4 -o StrictHostKeyChecking=no ${_sshPrefix(launchTarget.port)}${_portHost} ${JSON.stringify(_checkInner)}`
|
||||
: _checkInner;
|
||||
const _res = await fetch('/api/shell/exec', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
@@ -2131,20 +2146,8 @@ function _rerenderCachedModels() {
|
||||
// Resolve the target host from the visible Server dropdown — the reliable
|
||||
// source. Relying on _envState.remoteHost silently sent serves to Local
|
||||
// when that value was stale/empty. Pass it explicitly to the launcher.
|
||||
let serveHost = _envState.remoteHost || '';
|
||||
let _srvEnv = '', _srvEnvPath = '';
|
||||
const _ssEl = document.getElementById('hwfit-server-select') || document.getElementById('hwfit-dl-server');
|
||||
if (_ssEl && _ssEl.value != null) {
|
||||
if (_ssEl.value === 'local') serveHost = '';
|
||||
else {
|
||||
const _srv = _serverByVal?.(_ssEl.value) || _envState.servers[parseInt(_ssEl.value)];
|
||||
if (_srv) {
|
||||
serveHost = _srv.host;
|
||||
_srvEnv = _srv.env || '';
|
||||
_srvEnvPath = _srv.envPath || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
let serveHost = launchTarget.host || '';
|
||||
let _srvEnv = launchTarget.env || '', _srvEnvPath = launchTarget.venv || '';
|
||||
// The venv field wins; otherwise fall back to the env configured for the
|
||||
// selected server in Settings, so the activation isn't silently dropped
|
||||
// when the field is left blank (the per-server venv wasn't being applied).
|
||||
|
||||
@@ -68,6 +68,14 @@ def test_valid_remote_builds_port_flag():
|
||||
assert port_flag == "-p 2222 "
|
||||
|
||||
|
||||
def test_integer_ssh_port_in_stored_task_normalizes_without_crashing():
|
||||
host, port_flag = codex_routes._ssh_prefix_for_task(
|
||||
{"remoteHost": "user@box", "sshPort": 2222}
|
||||
)
|
||||
assert host == "user@box"
|
||||
assert port_flag == "-p 2222 "
|
||||
|
||||
|
||||
def test_default_ssh_port_omits_flag():
|
||||
host, port_flag = codex_routes._ssh_prefix_for_task(
|
||||
{"remoteHost": "box", "sshPort": "22"}
|
||||
|
||||
@@ -38,11 +38,14 @@ def test_diffusers_is_not_blocked_on_windows_dependencies_panel():
|
||||
assert "new Set(['diffusers'" not in text
|
||||
|
||||
|
||||
def test_diffusers_is_available_on_windows_serve_panel():
|
||||
def test_diffusers_is_available_only_on_local_windows_serve_panel():
|
||||
text = SERVE_SRC.read_text(encoding="utf-8")
|
||||
|
||||
assert "? ['llamacpp', 'diffusers']" in text
|
||||
assert "? [['llamacpp','llama.cpp'],['diffusers','Diffusers']]" in text
|
||||
assert "function _remoteWindowsDiffusersUnsupported(target)" in text
|
||||
assert "return !!(target?.host && target?.platform === 'windows');" in text
|
||||
assert "if (_remoteWindowsDiffusersUnsupported(target)) return [['llamacpp','llama.cpp']];" in text
|
||||
assert "return [['llamacpp','llama.cpp'],['diffusers','Diffusers']];" in text
|
||||
assert "Diffusers serving is not supported on remote Windows servers yet." in text
|
||||
|
||||
|
||||
def test_windows_diffusers_uses_python_not_python3():
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
import routes.cookbook_routes as cookbook_routes
|
||||
from routes.cookbook_helpers import ServeRequest
|
||||
|
||||
|
||||
def _route_endpoint(path: str, method: str):
|
||||
router = cookbook_routes.setup_cookbook_routes()
|
||||
for route in router.routes:
|
||||
if route.path == path and method in route.methods:
|
||||
return route.endpoint
|
||||
raise AssertionError(f"{method} {path} route not found")
|
||||
|
||||
|
||||
def _admin_request() -> Request:
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/api/model/serve",
|
||||
"headers": [],
|
||||
"state": {},
|
||||
}
|
||||
)
|
||||
request.state.current_user = "admin"
|
||||
return request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_windows_diffusers_is_rejected_before_runner_launch(monkeypatch):
|
||||
monkeypatch.setattr(cookbook_routes, "require_admin", lambda request: None)
|
||||
calls = []
|
||||
|
||||
async def fail_if_shell_runs(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
raise AssertionError("remote Windows Diffusers should fail before shell launch")
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", fail_if_shell_runs)
|
||||
|
||||
endpoint = _route_endpoint("/api/model/serve", "POST")
|
||||
req = ServeRequest(
|
||||
repo_id="diffusers/example",
|
||||
cmd="python scripts/diffusion_server.py --model diffusers/example --port 8100",
|
||||
remote_host="winbox",
|
||||
platform="windows",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await endpoint(_admin_request(), req)
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
assert "Remote Windows Diffusers" in str(exc.value.detail)
|
||||
assert calls == []
|
||||
@@ -36,10 +36,22 @@ def test_cookbook_submodules_resolve_visible_profile_selection():
|
||||
assert "_serverByVal(_envState.remoteServerKey || remoteHost)" in HWFIT
|
||||
assert "hk: _currentServerValue()" in HWFIT
|
||||
assert "sel.value = _currentServerValue();" in HWFIT
|
||||
assert "_serverByVal?.(_ssEl.value)" in SERVE
|
||||
assert "_serverByVal?.(select.value)" in SERVE
|
||||
assert "_serverByVal?.(val)" in SERVE
|
||||
assert "_serverByVal?.(_es.remoteServerKey || _es.remoteHost || '')" in SERVE
|
||||
assert "_serverByVal?.(_envState.remoteServerKey || _probeHost)" in SERVE
|
||||
assert "port: host ? (server?.port || _getPort(host) || '') : ''" in SERVE
|
||||
|
||||
|
||||
def test_serve_launch_preflights_use_selected_target_and_port():
|
||||
launch_target = "const launchTarget = _selectedServeTarget(panel);"
|
||||
assert launch_target in SERVE
|
||||
assert "const _hostStr = launchTarget.host || '';" in SERVE
|
||||
assert "const _probeHost = (launchTarget.host || '').trim();" in SERVE
|
||||
assert "if (launchTarget.port) _probeParams.set('ssh_port', launchTarget.port);" in SERVE
|
||||
assert "const _portHost = (launchTarget.host || '').trim();" in SERVE
|
||||
assert "StrictHostKeyChecking=no ${_sshPrefix(launchTarget.port)}${_portHost}" in SERVE
|
||||
assert "let serveHost = launchTarget.host || '';" in SERVE
|
||||
assert SERVE.index(launch_target) < SERVE.index("const _runningMod = await import('./cookbookRunning.js');")
|
||||
|
||||
|
||||
def test_running_tab_resolves_profile_key_not_first_host():
|
||||
|
||||
Reference in New Issue
Block a user