mirror of
https://github.com/ollama/ollama.git
synced 2026-04-18 09:03:35 -04:00
app/server: fix desktop app startup killing active ollama launch sessions
This commit is contained in:
@@ -18,11 +18,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
"github.com/ollama/ollama/app/logrotate"
|
||||
"github.com/ollama/ollama/app/store"
|
||||
)
|
||||
|
||||
const restartDelay = time.Second
|
||||
const externalServerHeartbeatTimeout = 2 * time.Second
|
||||
const externalServerHeartbeatInterval = time.Second
|
||||
const externalServerStartupWait = 5 * time.Second
|
||||
|
||||
// Server is a managed ollama server process
|
||||
type Server struct {
|
||||
@@ -152,6 +156,49 @@ func stop(proc *os.Process) error {
|
||||
}
|
||||
}
|
||||
|
||||
func externalServerAvailable(ctx context.Context) bool {
|
||||
client, err := api.ClientFromEnvironment()
|
||||
if err != nil {
|
||||
slog.Debug("failed to create ollama client", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
heartbeatCtx, cancel := context.WithTimeout(ctx, externalServerHeartbeatTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Heartbeat(heartbeatCtx); err != nil {
|
||||
slog.Debug("external ollama server heartbeat failed", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func waitForExternalServerAvailable(ctx context.Context, timeout time.Duration) bool {
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
|
||||
ticker := time.NewTicker(externalServerHeartbeatInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
if externalServerAvailable(ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-deadline.C:
|
||||
return false
|
||||
case <-ticker.C:
|
||||
if externalServerAvailable(ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
l, err := openRotatingLog()
|
||||
if err != nil {
|
||||
@@ -189,6 +236,12 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
if err = cmd.Wait(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && !s.dev && !reaped {
|
||||
if waitForExternalServerAvailable(ctx, externalServerStartupWait) {
|
||||
slog.Info("using existing ollama server")
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
reaped = true
|
||||
// This could be a port conflict, try to kill any existing ollama processes
|
||||
if err := reapServers(); err != nil {
|
||||
|
||||
@@ -4,6 +4,8 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -205,6 +207,37 @@ func TestServerCmdCloudSettingEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalServerAvailable(t *testing.T) {
|
||||
t.Run("healthy server", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodHead || r.URL.Path != "/" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
if !externalServerAvailable(t.Context()) {
|
||||
t.Fatal("expected external server to be available")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unhealthy server", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "nope", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
if externalServerAvailable(t.Context()) {
|
||||
t.Fatal("expected external server to be unavailable")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetInferenceInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -46,7 +46,22 @@ 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
|
||||
}
|
||||
|
||||
args := strings.Fields(strings.TrimSpace(string(output)))
|
||||
if len(args) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return filepath.Base(args[0]) == "ollama" && args[1] == "serve"
|
||||
}
|
||||
|
||||
// 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 +97,10 @@ func reapServers() error {
|
||||
if pid == currentPID {
|
||||
continue
|
||||
}
|
||||
if !ollamaServeProcess(pid) {
|
||||
slog.Debug("skipping non-server ollama process", "pid", pid)
|
||||
continue
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
|
||||
@@ -101,7 +101,34 @@ 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
|
||||
}
|
||||
|
||||
fields := strings.Fields(strings.ToLower(commandLine))
|
||||
for i, field := range fields {
|
||||
if strings.Trim(field, `"`) == "serve" && i > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +165,10 @@ func reapServers() error {
|
||||
if pid == currentPID {
|
||||
continue
|
||||
}
|
||||
if !ollamaServeProcess(pid) {
|
||||
slog.Debug("skipping non-server ollama process", "pid", pid)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := exec.Command("taskkill", "/F", "/PID", pidStr)
|
||||
if err := cmd.Run(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user