Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/lib/inference/ollama/windows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { createRequire } from "node:module";
import { describe, expect, it, vi } from "vitest";

const require = createRequire(import.meta.url);
const WINDOWS_DIST_PATH = require.resolve("../../../../dist/lib/inference/ollama/windows");
const RUNNER_PATH = require.resolve("../../../../dist/lib/runner");
const childProcess = require("node:child_process");

function commandText(command: string | string[]): string {
return Array.isArray(command) ? command.join(" ") : String(command);
}

function loadWindowsOllamaWithMocks(run: ReturnType<typeof vi.fn>, runCapture: ReturnType<typeof vi.fn>) {
const runner = require(RUNNER_PATH);
const originalRun = runner.run;
const originalRunCapture = runner.runCapture;
const originalSpawnSync = childProcess.spawnSync;

delete require.cache[WINDOWS_DIST_PATH];
runner.run = run;
runner.runCapture = runCapture;
childProcess.spawnSync = vi.fn(() => ({ status: 0 }));

return {
windows: require(WINDOWS_DIST_PATH),
restore() {
delete require.cache[WINDOWS_DIST_PATH];
runner.run = originalRun;
runner.runCapture = originalRunCapture;
childProcess.spawnSync = originalSpawnSync;
},
};
}

describe("Windows Ollama helper", () => {
it("falls back from a stale watcher path to the verified installed executable", () => {
const watcherPath = "C:\\Users\\tester\\AppData\\Local\\Programs\\Ollama\\ollama app.exe";
const installedPath = "C:\\Users\\tester\\AppData\\Local\\Programs\\Ollama\\ollama.exe";
const launchScripts: string[] = [];
const stopCommands: string[] = [];

const run = vi.fn((command: string[]) => {
const script = command[2] || "";
launchScripts.push(script);
if (script.includes(watcherPath)) {
return { status: 1, stderr: "stale watcher path" };
}
return { status: 0, stderr: "" };
});
const runCapture = vi.fn((command: string | string[]) => {
const cmd = commandText(command);
if (cmd.includes("Get-Process 'ollama app'") && cmd.includes("ExpandProperty Path")) {
return watcherPath;
}
if (cmd.includes("Stop-Process")) {
stopCommands.push(cmd);
return "";
}
if (cmd.includes("host.docker.internal:11434/api/tags")) {
return launchScripts.some((script) => script.includes(installedPath))
? JSON.stringify({ models: [] })
: "";
}
return "";
});
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const { windows, restore } = loadWindowsOllamaWithMocks(run, runCapture);

try {
expect(windows.setupWindowsOllamaWith0000Binding({ installedPath })).toBe(true);
} finally {
restore();
logSpy.mockRestore();
errorSpy.mockRestore();
}

expect(run).toHaveBeenCalledTimes(2);
expect(launchScripts[0]).toContain(watcherPath);
expect(launchScripts[1]).toContain(installedPath);
expect(launchScripts[1]).toContain("-ArgumentList 'serve'");
expect(launchScripts.some((script) => script.includes("Start-Process -FilePath ollama.exe"))).toBe(
false,
);
expect(stopCommands[0]).toContain("Get-Process 'ollama app'");
expect(stopCommands[1]).toContain("Get-Process ollama");
});
});
95 changes: 77 additions & 18 deletions src/lib/inference/ollama/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ function sleep(seconds: number): void {
spawnSync("sleep", [String(seconds)]);
}

function psSingleQuote(value: string): string {
return `'${String(value).replace(/'/g, "''")}'`;
}

// Pre-set OLLAMA_HOST in both User scope (persists across logins) and the
// current PowerShell session (inherited by the installer's auto-spawned
// ollama_app + daemon) so the new daemon binds 0.0.0.0 from the start.
Expand Down Expand Up @@ -132,48 +136,103 @@ function awaitWindowsOllamaReady(): boolean {
}

// Relaunch via the watcher path when available so the tray icon and the
// watcher's auto-restart survive; otherwise launch the daemon directly.
function launchAndAwaitWindowsOllama(watcherPath?: string): boolean {
// watcher's auto-restart survive; fall back through the verified installed
// path and finally refreshed PATH because stale watcher paths are possible.
function launchAndAwaitWindowsOllama(
opts: { watcherPath?: string; installedPath?: string } = {},
): boolean {
console.log(" Starting Ollama on Windows host via WSL interop...");
const launchScript = watcherPath
? `$env:OLLAMA_HOST='0.0.0.0:11434'; Start-Process -FilePath '${watcherPath.replace(/'/g, "''")}' -WindowStyle Hidden`
: "$env:OLLAMA_HOST='0.0.0.0:11434'; Start-Process -FilePath ollama.exe -ArgumentList serve -WindowStyle Hidden";
const result = run(["powershell.exe", "-Command", launchScript], {
ignoreError: true,
suppressOutput: true,
const watcherPath = typeof opts.watcherPath === "string" ? opts.watcherPath.trim() : "";
const installedPath = typeof opts.installedPath === "string" ? opts.installedPath.trim() : "";
const launchAttempts: Array<{ label: string; script: string }> = [];
if (watcherPath) {
launchAttempts.push({
label: "Ollama tray app",
script:
`$env:OLLAMA_HOST='0.0.0.0:11434'; Start-Process -FilePath ${psSingleQuote(watcherPath)} ` +
"-WindowStyle Hidden",
});
}
if (installedPath) {
launchAttempts.push({
label: "verified ollama.exe",
script:
`$env:OLLAMA_HOST='0.0.0.0:11434'; Start-Process -FilePath ${psSingleQuote(installedPath)} ` +
"-ArgumentList 'serve' -WindowStyle Hidden",
});
}
launchAttempts.push({
label: "refreshed Windows PATH",
script:
"$env:PATH = [Environment]::GetEnvironmentVariable('PATH','Machine') + ';' + [Environment]::GetEnvironmentVariable('PATH','User'); " +
"$env:OLLAMA_HOST='0.0.0.0:11434'; Start-Process -FilePath ollama.exe -ArgumentList serve -WindowStyle Hidden",
});
if (result.status !== 0) {

for (let i = 0; i < launchAttempts.length; i++) {
const attempt = launchAttempts[i];
const result = run(["powershell.exe", "-Command", attempt.script], {
ignoreError: true,
suppressOutput: true,
});
if (result.status === 0 && awaitWindowsOllamaReady()) {
return true;
}

const stderr = String(result.stderr || "").trim();
console.error(
` PowerShell launch failed (exit ${result.status})${stderr ? `: ${stderr}` : ""}`,
);
return false;
const error = result.error?.message;
const detail =
result.status === 0
? "Ollama did not become reachable"
: error || `exit ${result.status}${stderr ? `: ${stderr}` : ""}`;
console.error(` PowerShell launch via ${attempt.label} failed: ${detail}`);
if (i < launchAttempts.length - 1) {
killWindowsOllamaProcesses();
sleep(1);
}
}
return awaitWindowsOllamaReady();
return false;
}

// Used by start and restart paths to force a 0.0.0.0 binding on an already
// installed Ollama. Install path skips this: the installer's pre-set env
// already lands on the auto-spawned daemon.
function setupWindowsOllamaWith0000Binding(opts: { announceStop?: boolean } = {}): boolean {
// installed Ollama. Fresh install fallback passes installedPath to avoid
// relying on a newly-mutated Windows PATH from this process.
function setupWindowsOllamaWith0000Binding(
opts: { announceStop?: boolean; installedPath?: string } = {},
): boolean {
const watcherPath = captureWindowsOllamaWatcherPath();
persistOllamaHostEnvVar();
if (opts.announceStop) {
console.log(" Stopping existing Ollama on Windows host...");
}
killWindowsOllamaProcesses();
sleep(1);
return launchAndAwaitWindowsOllama(watcherPath || undefined);
return launchAndAwaitWindowsOllama({
watcherPath: watcherPath || undefined,
installedPath: opts.installedPath,
});
}

function switchToWindowsOllamaHost(): void {
setResolvedOllamaHost(OLLAMA_HOST_DOCKER_INTERNAL);
console.log(` ✓ Using Ollama on host.docker.internal:${OLLAMA_PORT}`);
}

function printWindowsOllamaTimeoutDiagnostics(): void {
console.error(" Timed out waiting for Ollama to start on the Windows host.");
console.error(" Diagnose Windows-side Ollama state with:");
console.error(' powershell.exe -Command "Get-Process ollama* -ErrorAction SilentlyContinue"');
console.error(
' powershell.exe -Command "Get-NetTCPConnection -LocalPort 11434 -State Listen -ErrorAction SilentlyContinue"',
);
console.error(
` curl -sS --connect-timeout 2 --max-time 5 http://host.docker.internal:${OLLAMA_PORT}/api/tags`,
);
}

module.exports = {
installOllamaOnWindowsHost,
awaitWindowsOllamaReady,
setupWindowsOllamaWith0000Binding,
switchToWindowsOllamaHost,
printWindowsOllamaTimeoutDiagnostics,
};
18 changes: 12 additions & 6 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ const {
awaitWindowsOllamaReady,
setupWindowsOllamaWith0000Binding,
switchToWindowsOllamaHost,
printWindowsOllamaTimeoutDiagnostics,
} = require("./inference/ollama/windows");
const { detectVllmProfile, installVllm } = require("./inference/vllm");
const inferenceConfig: typeof import("./inference/config") = require("./inference/config");
Expand Down Expand Up @@ -6934,8 +6935,6 @@ async function setupNim(
if (isSwitch) {
switchToWindowsOllamaHost();
} else if (isInstall) {
// installOllamaOnWindowsHost pre-sets the env so the auto-spawned
// daemon already binds 0.0.0.0; no kill+relaunch needed.
const installResult = await installOllamaOnWindowsHost();
if (!installResult.ok) {
console.error(
Expand All @@ -6945,14 +6944,21 @@ async function setupNim(
continue selectionLoop;
}
if (!awaitWindowsOllamaReady()) {
console.error(" Timed out waiting for Ollama to start on the Windows host.");
if (isNonInteractive()) process.exit(1);
continue selectionLoop;
console.log(" Installer did not leave a reachable Ollama daemon; restarting it...");
if (
!setupWindowsOllamaWith0000Binding({
installedPath: installResult.path,
})
) {
printWindowsOllamaTimeoutDiagnostics();
if (isNonInteractive()) process.exit(1);
continue selectionLoop;
}
}
console.log(` ✓ Using Ollama on host.docker.internal:${OLLAMA_PORT}`);
} else {
if (!setupWindowsOllamaWith0000Binding({ announceStop: isRestart })) {
console.error(" Timed out waiting for Ollama to start on the Windows host.");
printWindowsOllamaTimeoutDiagnostics();
if (isNonInteractive()) process.exit(1);
continue selectionLoop;
}
Expand Down
Loading
Loading