server.go 4.0 KB

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