123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- package lifecycle
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "log/slog"
- "os"
- "os/exec"
- "path/filepath"
- "time"
- "github.com/ollama/ollama/api"
- )
- func getCLIFullPath(command string) string {
- var cmdPath string
- appExe, err := os.Executable()
- if err == nil {
- // Check both the same location as the tray app, as well as ./bin
- cmdPath = filepath.Join(filepath.Dir(appExe), command)
- _, err := os.Stat(cmdPath)
- if err == nil {
- return cmdPath
- }
- cmdPath = filepath.Join(filepath.Dir(appExe), "bin", command)
- _, err = os.Stat(cmdPath)
- if err == nil {
- return cmdPath
- }
- }
- cmdPath, err = exec.LookPath(command)
- if err == nil {
- _, err := os.Stat(cmdPath)
- if err == nil {
- return cmdPath
- }
- }
- pwd, err := os.Getwd()
- if err == nil {
- cmdPath = filepath.Join(pwd, command)
- _, err = os.Stat(cmdPath)
- if err == nil {
- return cmdPath
- }
- }
- return command
- }
- func start(ctx context.Context, command string) (*exec.Cmd, error) {
- cmd := getCmd(ctx, getCLIFullPath(command))
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err)
- }
- stderr, err := cmd.StderrPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", err)
- }
- rotateLogs(ServerLogFile)
- logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755)
- if err != nil {
- return nil, fmt.Errorf("failed to create server log: %w", err)
- }
- logDir := filepath.Dir(ServerLogFile)
- _, err = os.Stat(logDir)
- if err != nil {
- if !errors.Is(err, os.ErrNotExist) {
- return nil, fmt.Errorf("stat ollama server log dir %s: %v", logDir, err)
- }
- if err := os.MkdirAll(logDir, 0o755); err != nil {
- return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
- }
- }
- go func() {
- defer logFile.Close()
- io.Copy(logFile, stdout) //nolint:errcheck
- }()
- go func() {
- defer logFile.Close()
- io.Copy(logFile, stderr) //nolint:errcheck
- }()
- // Re-wire context done behavior to attempt a graceful shutdown of the server
- cmd.Cancel = func() error {
- if cmd.Process != nil {
- err := terminate(cmd)
- if err != nil {
- slog.Warn("error trying to gracefully terminate server", "err", err)
- return cmd.Process.Kill()
- }
- tick := time.NewTicker(10 * time.Millisecond)
- defer tick.Stop()
- for {
- select {
- case <-tick.C:
- exited, err := isProcessExited(cmd.Process.Pid)
- if err != nil {
- return err
- }
- if exited {
- return nil
- }
- case <-time.After(5 * time.Second):
- slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid)
- return cmd.Process.Kill()
- }
- }
- }
- return nil
- }
- // run the command and wait for it to finish
- if err := cmd.Start(); err != nil {
- return nil, fmt.Errorf("failed to start server %w", err)
- }
- if cmd.Process != nil {
- slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
- }
- slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
- return cmd, nil
- }
- func SpawnServer(ctx context.Context, command string) (chan int, error) {
- done := make(chan int)
- go func() {
- // Keep the server running unless we're shuttind down the app
- crashCount := 0
- for {
- slog.Info("starting server...")
- cmd, err := start(ctx, command)
- if err != nil {
- crashCount++
- slog.Error(fmt.Sprintf("failed to start server %s", err))
- time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
- continue
- }
- cmd.Wait() //nolint:errcheck
- var code int
- if cmd.ProcessState != nil {
- code = cmd.ProcessState.ExitCode()
- }
- select {
- case <-ctx.Done():
- slog.Info(fmt.Sprintf("server shutdown with exit code %d", code))
- done <- code
- return
- default:
- crashCount++
- slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
- time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
- break
- }
- }
- }()
- return done, nil
- }
- func IsServerRunning(ctx context.Context) bool {
- client, err := api.ClientFromEnvironment()
- if err != nil {
- slog.Info("unable to connect to server")
- return false
- }
- err = client.Heartbeat(ctx)
- if err != nil {
- slog.Debug(fmt.Sprintf("heartbeat from server: %s", err))
- slog.Info("unable to connect to server")
- return false
- }
- return true
- }
|