server.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package lifecycle
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "syscall"
  12. "time"
  13. "github.com/ollama/ollama/api"
  14. )
  15. func getCLIFullPath(command string) string {
  16. cmdPath := ""
  17. appExe, err := os.Executable()
  18. if err == nil {
  19. cmdPath = filepath.Join(filepath.Dir(appExe), command)
  20. _, err := os.Stat(cmdPath)
  21. if err == nil {
  22. return cmdPath
  23. }
  24. }
  25. cmdPath, err = exec.LookPath(command)
  26. if err == nil {
  27. _, err := os.Stat(cmdPath)
  28. if err == nil {
  29. return cmdPath
  30. }
  31. }
  32. pwd, err := os.Getwd()
  33. if err == nil {
  34. cmdPath = filepath.Join(pwd, command)
  35. _, err = os.Stat(cmdPath)
  36. if err == nil {
  37. return cmdPath
  38. }
  39. }
  40. return command
  41. }
  42. func SpawnServer(ctx context.Context, command string) (chan int, error) {
  43. done := make(chan int)
  44. logDir := filepath.Dir(ServerLogFile)
  45. _, err := os.Stat(logDir)
  46. if errors.Is(err, os.ErrNotExist) {
  47. if err := os.MkdirAll(logDir, 0o755); err != nil {
  48. return done, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
  49. }
  50. }
  51. cmd := getCmd(ctx, getCLIFullPath(command))
  52. // send stdout and stderr to a file
  53. stdout, err := cmd.StdoutPipe()
  54. if err != nil {
  55. return done, fmt.Errorf("failed to spawn server stdout pipe %s", err)
  56. }
  57. stderr, err := cmd.StderrPipe()
  58. if err != nil {
  59. return done, fmt.Errorf("failed to spawn server stderr pipe %s", err)
  60. }
  61. stdin, err := cmd.StdinPipe()
  62. if err != nil {
  63. return done, fmt.Errorf("failed to spawn server stdin pipe %s", err)
  64. }
  65. // TODO - rotation
  66. logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
  67. if err != nil {
  68. return done, fmt.Errorf("failed to create server log %w", err)
  69. }
  70. go func() {
  71. defer logFile.Close()
  72. io.Copy(logFile, stdout) //nolint:errcheck
  73. }()
  74. go func() {
  75. defer logFile.Close()
  76. io.Copy(logFile, stderr) //nolint:errcheck
  77. }()
  78. // Re-wire context done behavior to attempt a graceful shutdown of the server
  79. cmd.Cancel = func() error {
  80. if cmd.Process != nil {
  81. cmd.Process.Signal(os.Interrupt) //nolint:errcheck
  82. tick := time.NewTicker(10 * time.Millisecond)
  83. defer tick.Stop()
  84. for {
  85. select {
  86. case <-tick.C:
  87. // OS agnostic "is it still running"
  88. if proc, err := os.FindProcess(int(cmd.Process.Pid)); err != nil || errors.Is(proc.Signal(syscall.Signal(0)), os.ErrProcessDone) {
  89. return nil //nolint:nilerr
  90. }
  91. case <-time.After(5 * time.Second):
  92. slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid)
  93. cmd.Process.Kill() //nolint:errcheck
  94. }
  95. }
  96. }
  97. return nil
  98. }
  99. // run the command and wait for it to finish
  100. if err := cmd.Start(); err != nil {
  101. return done, fmt.Errorf("failed to start server %w", err)
  102. }
  103. if cmd.Process != nil {
  104. slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
  105. }
  106. slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
  107. go func() {
  108. // Keep the server running unless we're shuttind down the app
  109. crashCount := 0
  110. for {
  111. cmd.Wait() //nolint:errcheck
  112. stdin.Close()
  113. var code int
  114. if cmd.ProcessState != nil {
  115. code = cmd.ProcessState.ExitCode()
  116. }
  117. select {
  118. case <-ctx.Done():
  119. slog.Info(fmt.Sprintf("server shutdown with exit code %d", code))
  120. done <- code
  121. return
  122. default:
  123. crashCount++
  124. slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
  125. time.Sleep(500 * time.Millisecond)
  126. if err := cmd.Start(); err != nil {
  127. slog.Error(fmt.Sprintf("failed to restart server %s", err))
  128. // Keep trying, but back off if we keep failing
  129. time.Sleep(time.Duration(crashCount) * time.Second)
  130. }
  131. }
  132. }
  133. }()
  134. return done, nil
  135. }
  136. func IsServerRunning(ctx context.Context) bool {
  137. client, err := api.ClientFromEnvironment()
  138. if err != nil {
  139. slog.Info("unable to connect to server")
  140. return false
  141. }
  142. err = client.Heartbeat(ctx)
  143. if err != nil {
  144. slog.Debug(fmt.Sprintf("heartbeat from server: %s", err))
  145. slog.Info("unable to connect to server")
  146. return false
  147. }
  148. return true
  149. }