Skip to content

Commit 7027f26

Browse files
authored
app: gracefully shut down ollama serve on windows (ollama#3641)
* app: gracefully shut down `ollama serve` on windows * fix linter errors * bring back `HideWindow` * remove creation flags * restore `windows.CREATE_NEW_PROCESS_GROUP`
1 parent 9bee3b6 commit 7027f26

File tree

3 files changed

+118
-7
lines changed

3 files changed

+118
-7
lines changed

app/lifecycle/server.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os"
1010
"os/exec"
1111
"path/filepath"
12-
"syscall"
1312
"time"
1413

1514
"github.com/ollama/ollama/api"
@@ -87,19 +86,29 @@ func SpawnServer(ctx context.Context, command string) (chan int, error) {
8786
// Re-wire context done behavior to attempt a graceful shutdown of the server
8887
cmd.Cancel = func() error {
8988
if cmd.Process != nil {
90-
cmd.Process.Signal(os.Interrupt) //nolint:errcheck
89+
err := terminate(cmd)
90+
if err != nil {
91+
slog.Warn("error trying to gracefully terminate server", "err", err)
92+
return cmd.Process.Kill()
93+
}
94+
9195
tick := time.NewTicker(10 * time.Millisecond)
9296
defer tick.Stop()
97+
9398
for {
9499
select {
95100
case <-tick.C:
96-
// OS agnostic "is it still running"
97-
if proc, err := os.FindProcess(int(cmd.Process.Pid)); err != nil || errors.Is(proc.Signal(syscall.Signal(0)), os.ErrProcessDone) {
98-
return nil //nolint:nilerr
101+
exited, err := isProcessExited(cmd.Process.Pid)
102+
if err != nil {
103+
return err
104+
}
105+
106+
if exited {
107+
return nil
99108
}
100109
case <-time.After(5 * time.Second):
101110
slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid)
102-
cmd.Process.Kill() //nolint:errcheck
111+
return cmd.Process.Kill()
103112
}
104113
}
105114
}

app/lifecycle/server_unix.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,35 @@ package lifecycle
44

55
import (
66
"context"
7+
"errors"
8+
"fmt"
9+
"os"
710
"os/exec"
11+
"syscall"
812
)
913

1014
func getCmd(ctx context.Context, cmd string) *exec.Cmd {
1115
return exec.CommandContext(ctx, cmd, "serve")
1216
}
17+
18+
func terminate(cmd *exec.Cmd) error {
19+
return cmd.Process.Signal(os.Interrupt)
20+
}
21+
22+
func isProcessExited(pid int) (bool, error) {
23+
proc, err := os.FindProcess(pid)
24+
if err != nil {
25+
return false, fmt.Errorf("failed to find process: %v", err)
26+
}
27+
28+
err = proc.Signal(syscall.Signal(0))
29+
if err != nil {
30+
if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
31+
return true, nil
32+
}
33+
34+
return false, fmt.Errorf("error signaling process: %v", err)
35+
}
36+
37+
return false, nil
38+
}

app/lifecycle/server_windows.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,88 @@ package lifecycle
22

33
import (
44
"context"
5+
"fmt"
56
"os/exec"
67
"syscall"
8+
9+
"golang.org/x/sys/windows"
710
)
811

912
func getCmd(ctx context.Context, exePath string) *exec.Cmd {
1013
cmd := exec.CommandContext(ctx, exePath, "serve")
11-
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
14+
cmd.SysProcAttr = &syscall.SysProcAttr{
15+
HideWindow: true,
16+
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP,
17+
}
18+
1219
return cmd
1320
}
21+
22+
func terminate(cmd *exec.Cmd) error {
23+
dll, err := windows.LoadDLL("kernel32.dll")
24+
if err != nil {
25+
return err
26+
}
27+
defer dll.Release() // nolint: errcheck
28+
29+
pid := cmd.Process.Pid
30+
31+
f, err := dll.FindProc("AttachConsole")
32+
if err != nil {
33+
return err
34+
}
35+
36+
r1, _, err := f.Call(uintptr(pid))
37+
if r1 == 0 && err != syscall.ERROR_ACCESS_DENIED {
38+
return err
39+
}
40+
41+
f, err = dll.FindProc("SetConsoleCtrlHandler")
42+
if err != nil {
43+
return err
44+
}
45+
46+
r1, _, err = f.Call(0, 1)
47+
if r1 == 0 {
48+
return err
49+
}
50+
51+
f, err = dll.FindProc("GenerateConsoleCtrlEvent")
52+
if err != nil {
53+
return err
54+
}
55+
56+
r1, _, err = f.Call(windows.CTRL_BREAK_EVENT, uintptr(pid))
57+
if r1 == 0 {
58+
return err
59+
}
60+
61+
r1, _, err = f.Call(windows.CTRL_C_EVENT, uintptr(pid))
62+
if r1 == 0 {
63+
return err
64+
}
65+
66+
return nil
67+
}
68+
69+
const STILL_ACTIVE = 259
70+
71+
func isProcessExited(pid int) (bool, error) {
72+
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
73+
if err != nil {
74+
return false, fmt.Errorf("failed to open process: %v", err)
75+
}
76+
defer windows.CloseHandle(hProcess) // nolint: errcheck
77+
78+
var exitCode uint32
79+
err = windows.GetExitCodeProcess(hProcess, &exitCode)
80+
if err != nil {
81+
return false, fmt.Errorf("failed to get exit code: %v", err)
82+
}
83+
84+
if exitCode == STILL_ACTIVE {
85+
return false, nil
86+
}
87+
88+
return true, nil
89+
}

0 commit comments

Comments
 (0)