server.go 3.9 KB

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