runtime.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package runtime
  2. import (
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "strings"
  10. "syscall"
  11. "time"
  12. "github.com/goccy/go-json"
  13. )
  14. var BaseDir = filepath.Join(os.TempDir(), "pizzasql")
  15. type KVInfo struct {
  16. PID int `json:"pid"`
  17. Port int `json:"port"`
  18. Addr string `json:"addr"`
  19. }
  20. type ProcessInfo struct {
  21. PID int `json:"pid"`
  22. HTTPPort int `json:"http_port,omitempty"`
  23. PGPort int `json:"pg_port,omitempty"`
  24. }
  25. type Info struct {
  26. PizzaSQL *ProcessInfo `json:"pizzasql,omitempty"`
  27. PizzaKV *KVInfo `json:"pizzakv,omitempty"`
  28. }
  29. func instanceDir(pid int) string {
  30. return filepath.Join(BaseDir, strconv.Itoa(pid))
  31. }
  32. func instanceFile(pid int) string {
  33. return filepath.Join(instanceDir(pid), "runtime.json")
  34. }
  35. // File returns the runtime file path for the current process.
  36. var File = instanceFile(os.Getpid())
  37. func loadFile(path string) (*Info, error) {
  38. data, err := os.ReadFile(path)
  39. if err != nil {
  40. if os.IsNotExist(err) {
  41. return &Info{}, nil
  42. }
  43. return nil, err
  44. }
  45. var info Info
  46. if err := json.Unmarshal(data, &info); err != nil {
  47. os.Remove(path)
  48. return &Info{}, nil
  49. }
  50. return &info, nil
  51. }
  52. func Load() (*Info, error) {
  53. return loadFile(instanceFile(os.Getpid()))
  54. }
  55. func write(info *Info) error {
  56. dir := instanceDir(os.Getpid())
  57. if err := os.MkdirAll(dir, 0755); err != nil {
  58. return err
  59. }
  60. data, err := json.MarshalIndent(info, "", " ")
  61. if err != nil {
  62. return err
  63. }
  64. return os.WriteFile(instanceFile(os.Getpid()), data, 0644)
  65. }
  66. func pidAlive(pid int) bool {
  67. p, err := os.FindProcess(pid)
  68. if err != nil {
  69. return false
  70. }
  71. return p.Signal(syscall.Signal(0)) == nil
  72. }
  73. func addrResponds(addr string) bool {
  74. network, target := "tcp", addr
  75. if strings.HasPrefix(addr, "unix:") {
  76. network, target = "unix", strings.TrimPrefix(addr, "unix:")
  77. }
  78. conn, err := net.DialTimeout(network, target, time.Second)
  79. if err != nil {
  80. return false
  81. }
  82. conn.Close()
  83. return true
  84. }
  85. // LiveInstances returns all runtime files from other instances that have a live pizzasql PID.
  86. func LiveInstances() []*Info {
  87. entries, err := os.ReadDir(BaseDir)
  88. if err != nil {
  89. return nil
  90. }
  91. selfPID := os.Getpid()
  92. var live []*Info
  93. for _, e := range entries {
  94. if !e.IsDir() {
  95. continue
  96. }
  97. pid, err := strconv.Atoi(e.Name())
  98. if err != nil || pid == selfPID {
  99. continue
  100. }
  101. path := filepath.Join(BaseDir, e.Name(), "runtime.json")
  102. info, err := loadFile(path)
  103. if err != nil || info.PizzaSQL == nil {
  104. continue
  105. }
  106. if pidAlive(info.PizzaSQL.PID) {
  107. live = append(live, info)
  108. } else {
  109. os.RemoveAll(filepath.Join(BaseDir, e.Name()))
  110. }
  111. }
  112. return live
  113. }
  114. // CheckExistingInstances warns about live instances and prompts the user.
  115. // Returns an error only if the user declines to continue.
  116. func CheckExistingInstances(force bool) error {
  117. live := LiveInstances()
  118. if len(live) == 0 {
  119. return nil
  120. }
  121. fmt.Fprintf(os.Stderr, "Warning: %d pizzasql instance(s) already running:\n", len(live))
  122. for _, info := range live {
  123. extra := ""
  124. if info.PizzaSQL.HTTPPort != 0 {
  125. extra += fmt.Sprintf(" http=:%d", info.PizzaSQL.HTTPPort)
  126. }
  127. if info.PizzaSQL.PGPort != 0 {
  128. extra += fmt.Sprintf(" pg=:%d", info.PizzaSQL.PGPort)
  129. }
  130. if info.PizzaKV != nil {
  131. extra += fmt.Sprintf(" kv=%s", info.PizzaKV.Addr)
  132. }
  133. fmt.Fprintf(os.Stderr, " PID %d%s\n", info.PizzaSQL.PID, extra)
  134. }
  135. if force {
  136. fmt.Fprintf(os.Stderr, "Auto-continuing (-y flag set)\n")
  137. return nil
  138. }
  139. fmt.Fprintf(os.Stderr, "Continue anyway? [y/N] ")
  140. reader := bufio.NewReader(os.Stdin)
  141. line, _ := reader.ReadString('\n')
  142. if line != "y\n" && line != "Y\n" {
  143. return fmt.Errorf("aborted")
  144. }
  145. return nil
  146. }
  147. // WritePizzaSQL records the pizzasql process in this instance's runtime file.
  148. func WritePizzaSQL(pid, httpPort, pgPort int) error {
  149. info, err := Load()
  150. if err != nil {
  151. info = &Info{}
  152. }
  153. info.PizzaSQL = &ProcessInfo{PID: pid, HTTPPort: httpPort, PGPort: pgPort}
  154. return write(info)
  155. }
  156. // WriteKV records the pizzakv process in this instance's runtime file.
  157. func WriteKV(kv *KVInfo) error {
  158. info, err := Load()
  159. if err != nil {
  160. info = &Info{}
  161. }
  162. info.PizzaKV = kv
  163. return write(info)
  164. }
  165. // Cleanup removes this instance's runtime directory.
  166. func Cleanup() {
  167. os.RemoveAll(instanceDir(os.Getpid()))
  168. }