server.go 4.2 KB

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