diff --git a/app/server/server.go b/app/server/server.go index 89a8df58f..ef5c0576d 100644 --- a/app/server/server.go +++ b/app/server/server.go @@ -83,6 +83,29 @@ func resolvePath(name string) string { return name } +func ollamaServeArgs(args []string) bool { + if len(args) < 2 { + return false + } + + switch strings.Trim(filepath.Base(args[0]), `"`) { + case "ollama", "ollama.exe": + default: + return false + } + + for _, rawArg := range args[1:] { + arg := strings.Trim(rawArg, `"`) + if strings.HasPrefix(arg, "-") { + continue + } + + return arg == "serve" || arg == "start" + } + + return false +} + // cleanup checks the pid file for a running ollama process // and shuts it down gracefully if it is running func cleanup() error { diff --git a/app/server/server_test.go b/app/server/server_test.go index 34d12dfd6..cb9674229 100644 --- a/app/server/server_test.go +++ b/app/server/server_test.go @@ -205,6 +205,63 @@ func TestServerCmdCloudSettingEnv(t *testing.T) { } } +func TestOllamaServeArgs(t *testing.T) { + tests := []struct { + name string + args []string + want bool + }{ + { + name: "system ollama serve", + args: []string{"ollama", "serve"}, + want: true, + }, + { + name: "relative path ollama serve", + args: []string{"./ollama", "serve"}, + want: true, + }, + { + name: "serve after other flags", + args: []string{"./ollama", "--verbose", "serve"}, + want: true, + }, + { + name: "start alias", + args: []string{"ollama", "start"}, + want: true, + }, + { + name: "launch command", + args: []string{"ollama", "launch", "opencode"}, + want: false, + }, + { + name: "run command with model named serve", + args: []string{"ollama", "run", "serve"}, + want: false, + }, + { + name: "launch command with serve in passthrough args", + args: []string{"ollama", "launch", "codex", "--", "-p", "serve"}, + want: false, + }, + { + name: "different executable", + args: []string{"go", "run", "serve"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ollamaServeArgs(tt.args); got != tt.want { + t.Fatalf("ollamaServeArgs(%v) = %v, want %v", tt.args, got, tt.want) + } + }) + } +} + func TestGetInferenceInfo(t *testing.T) { tests := []struct { name string diff --git a/app/server/server_unix.go b/app/server/server_unix.go index 2c50716b2..1f40f5b2a 100644 --- a/app/server/server_unix.go +++ b/app/server/server_unix.go @@ -46,7 +46,17 @@ func terminated(pid int) (bool, error) { return false, nil } -// reapServers kills all ollama processes except our own +func ollamaServeProcess(pid int) bool { + output, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=").Output() + if err != nil { + slog.Debug("failed to inspect ollama process", "pid", pid, "err", err) + return false + } + + return ollamaServeArgs(strings.Fields(strings.TrimSpace(string(output)))) +} + +// reapServers kills external ollama serve processes except our own. func reapServers() error { // Get our own PID to avoid killing ourselves currentPID := os.Getpid() @@ -82,6 +92,9 @@ func reapServers() error { if pid == currentPID { continue } + if !ollamaServeProcess(pid) { + continue + } proc, err := os.FindProcess(pid) if err != nil { diff --git a/app/server/server_windows.go b/app/server/server_windows.go index c2e7f4b9e..06d7ba629 100644 --- a/app/server/server_windows.go +++ b/app/server/server_windows.go @@ -101,7 +101,29 @@ func terminated(pid int) (bool, error) { return true, nil } -// reapServers kills all ollama processes except our own +func ollamaServeProcess(pid int) bool { + cmd := exec.Command("wmic", "process", "where", fmt.Sprintf("ProcessId=%d", pid), "get", "CommandLine", "/value") + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + output, err := cmd.Output() + if err != nil { + slog.Debug("failed to inspect ollama process", "pid", pid, "err", err) + return false + } + + for _, line := range strings.Split(string(output), "\n") { + line = strings.TrimSpace(line) + commandLine, ok := strings.CutPrefix(line, "CommandLine=") + if !ok { + continue + } + + return ollamaServeArgs(strings.Fields(strings.ToLower(commandLine))) + } + + return false +} + +// reapServers kills external ollama serve processes except our own. func reapServers() error { // Get current process ID to avoid killing ourselves currentPID := os.Getpid() @@ -138,6 +160,9 @@ func reapServers() error { if pid == currentPID { continue } + if !ollamaServeProcess(pid) { + continue + } cmd := exec.Command("taskkill", "/F", "/PID", pidStr) if err := cmd.Run(); err != nil {