kvmanager.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package kvmanager
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "strconv"
  10. "strings"
  11. "syscall"
  12. "time"
  13. )
  14. // KVInfo contains information about the running PizzaKV instance
  15. type KVInfo struct {
  16. PID int `json:"pid"`
  17. Port int `json:"port"`
  18. Addr string `json:"addr"`
  19. }
  20. // Manager handles the lifecycle of a PizzaKV process
  21. type Manager struct {
  22. cmd *exec.Cmd
  23. infoFile string
  24. info *KVInfo
  25. }
  26. // NewManager creates a new KVManager
  27. func NewManager() *Manager {
  28. return &Manager{
  29. infoFile: ".pizzakv.json",
  30. }
  31. }
  32. // SetInfoFile sets a custom path for the info file
  33. func (m *Manager) SetInfoFile(path string) {
  34. m.infoFile = path
  35. }
  36. // Start launches pizzakv with the given flags on a random available port
  37. func (m *Manager) Start(kvFlags string) (*KVInfo, error) {
  38. // Find an available port between 1024-9999
  39. port, err := findAvailablePortInRange(1024, 9999)
  40. if err != nil {
  41. return nil, fmt.Errorf("failed to find available port: %w", err)
  42. }
  43. // Build the command arguments
  44. // PizzaKV uses -port=XXXX format (single dash)
  45. args := []string{fmt.Sprintf("-port=%d", port)}
  46. // Parse and add custom flags if provided
  47. if kvFlags != "" {
  48. customArgs := parseFlags(kvFlags)
  49. args = append(args, customArgs...)
  50. }
  51. // Create the command
  52. cmd := exec.Command("pizzakv", args...)
  53. // Set up process group to allow clean shutdown
  54. cmd.SysProcAttr = &syscall.SysProcAttr{
  55. Setpgid: true,
  56. }
  57. // Redirect output to /dev/null or capture it
  58. cmd.Stdout = os.Stdout
  59. cmd.Stderr = os.Stderr
  60. // Start the process
  61. if err := cmd.Start(); err != nil {
  62. return nil, fmt.Errorf("failed to start pizzakv: %w", err)
  63. }
  64. m.cmd = cmd
  65. m.info = &KVInfo{
  66. PID: cmd.Process.Pid,
  67. Port: port,
  68. Addr: fmt.Sprintf("localhost:%d", port),
  69. }
  70. // Wait for the process to start and begin listening
  71. // We need to wait longer to ensure PizzaKV is actually listening
  72. time.Sleep(500 * time.Millisecond)
  73. // Check if process is still running
  74. if !m.IsRunning() {
  75. return nil, fmt.Errorf("pizzakv process exited immediately after starting")
  76. }
  77. fmt.Println("Waiting for PizzaKV to be ready...")
  78. // Wait for PizzaKV to be ready (finish restoring records, etc.)
  79. if err := m.waitForReady(port, 30*time.Second); err != nil {
  80. m.Stop()
  81. return nil, fmt.Errorf("pizzakv did not become ready: %w", err)
  82. }
  83. // Write info to file
  84. if err := m.writeInfoFile(); err != nil {
  85. m.Stop()
  86. return nil, fmt.Errorf("failed to write info file: %w", err)
  87. }
  88. return m.info, nil
  89. }
  90. // Stop stops the pizzakv process
  91. func (m *Manager) Stop() error {
  92. if m.cmd == nil || m.cmd.Process == nil {
  93. return nil
  94. }
  95. // Try graceful shutdown first
  96. if err := m.cmd.Process.Signal(syscall.SIGTERM); err != nil {
  97. // If SIGTERM fails, try SIGKILL
  98. if err := m.cmd.Process.Kill(); err != nil {
  99. return fmt.Errorf("failed to kill process: %w", err)
  100. }
  101. }
  102. // Wait for process to exit with timeout
  103. done := make(chan error, 1)
  104. go func() {
  105. _, err := m.cmd.Process.Wait()
  106. done <- err
  107. }()
  108. select {
  109. case <-done:
  110. // Process exited
  111. case <-time.After(5 * time.Second):
  112. // Timeout, force kill
  113. m.cmd.Process.Kill()
  114. }
  115. // Clean up info file
  116. os.Remove(m.infoFile)
  117. return nil
  118. }
  119. // IsRunning checks if the pizzakv process is still running
  120. func (m *Manager) IsRunning() bool {
  121. if m.cmd == nil || m.cmd.Process == nil {
  122. return false
  123. }
  124. // Send signal 0 to check if process exists
  125. err := m.cmd.Process.Signal(syscall.Signal(0))
  126. return err == nil
  127. }
  128. // waitForReady waits for PizzaKV to be ready to accept connections
  129. func (m *Manager) waitForReady(port int, timeout time.Duration) error {
  130. addr := fmt.Sprintf("127.0.0.1:%d", port)
  131. deadline := time.Now().Add(timeout)
  132. for time.Now().Before(deadline) {
  133. // Check if process is still running
  134. if !m.IsRunning() {
  135. return fmt.Errorf("process died while waiting for ready")
  136. }
  137. // Try to connect
  138. conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
  139. if err == nil {
  140. conn.Close()
  141. // Successfully connected, PizzaKV is ready
  142. return nil
  143. }
  144. // Wait a bit before retrying
  145. time.Sleep(100 * time.Millisecond)
  146. }
  147. return fmt.Errorf("timeout waiting for PizzaKV to become ready on port %d", port)
  148. }
  149. // GetInfo returns the KVInfo for the running instance
  150. func (m *Manager) GetInfo() *KVInfo {
  151. return m.info
  152. }
  153. // LoadInfo loads KVInfo from the info file
  154. func (m *Manager) LoadInfo() (*KVInfo, error) {
  155. data, err := os.ReadFile(m.infoFile)
  156. if err != nil {
  157. return nil, fmt.Errorf("failed to read info file: %w", err)
  158. }
  159. var info KVInfo
  160. if err := json.Unmarshal(data, &info); err != nil {
  161. return nil, fmt.Errorf("failed to parse info file: %w", err)
  162. }
  163. return &info, nil
  164. }
  165. // writeInfoFile writes the KVInfo to a file
  166. func (m *Manager) writeInfoFile() error {
  167. data, err := json.MarshalIndent(m.info, "", " ")
  168. if err != nil {
  169. return fmt.Errorf("failed to marshal info: %w", err)
  170. }
  171. // Create directory if it doesn't exist
  172. dir := filepath.Dir(m.infoFile)
  173. if dir != "." {
  174. if err := os.MkdirAll(dir, 0755); err != nil {
  175. return fmt.Errorf("failed to create directory: %w", err)
  176. }
  177. }
  178. if err := os.WriteFile(m.infoFile, data, 0644); err != nil {
  179. return fmt.Errorf("failed to write info file: %w", err)
  180. }
  181. return nil
  182. }
  183. // findAvailablePort finds a random available port (kept for compatibility)
  184. func findAvailablePort() (int, error) {
  185. return findAvailablePortInRange(1024, 65535)
  186. }
  187. // findAvailablePortInRange finds a random available port within the specified range
  188. func findAvailablePortInRange(minPort, maxPort int) (int, error) {
  189. // Try up to 100 times to find an available port
  190. for i := 0; i < 100; i++ {
  191. // Generate random port in range
  192. port := minPort + (int(time.Now().UnixNano()) % (maxPort - minPort + 1))
  193. // Try to listen on this port
  194. addr := fmt.Sprintf("127.0.0.1:%d", port)
  195. listener, err := net.Listen("tcp", addr)
  196. if err != nil {
  197. // Port is in use, try another
  198. continue
  199. }
  200. defer listener.Close()
  201. // Port is available
  202. return port, nil
  203. }
  204. return 0, fmt.Errorf("could not find available port in range %d-%d after 100 attempts", minPort, maxPort)
  205. }
  206. // parseFlags parses a flag string like "-iwal -port=9090" into a slice of strings
  207. func parseFlags(flags string) []string {
  208. // Trim whitespace
  209. flags = strings.TrimSpace(flags)
  210. if flags == "" {
  211. return nil
  212. }
  213. var result []string
  214. var current strings.Builder
  215. inQuote := false
  216. for i, r := range flags {
  217. switch r {
  218. case '"', '\'':
  219. inQuote = !inQuote
  220. case ' ':
  221. if !inQuote {
  222. if current.Len() > 0 {
  223. result = append(result, current.String())
  224. current.Reset()
  225. }
  226. } else {
  227. current.WriteRune(r)
  228. }
  229. default:
  230. current.WriteRune(r)
  231. }
  232. // Handle last character
  233. if i == len(flags)-1 && current.Len() > 0 {
  234. result = append(result, current.String())
  235. }
  236. }
  237. return result
  238. }
  239. // CleanupStaleProcess checks if there's a stale PID file and cleans it up
  240. func CleanupStaleProcess(infoFile string) error {
  241. data, err := os.ReadFile(infoFile)
  242. if err != nil {
  243. if os.IsNotExist(err) {
  244. return nil // No file, nothing to clean
  245. }
  246. return err
  247. }
  248. var info KVInfo
  249. if err := json.Unmarshal(data, &info); err != nil {
  250. // Invalid file, just remove it
  251. return os.Remove(infoFile)
  252. }
  253. // Check if process is still running
  254. process, err := os.FindProcess(info.PID)
  255. if err != nil {
  256. // Process doesn't exist, remove file
  257. return os.Remove(infoFile)
  258. }
  259. // Try to signal the process
  260. err = process.Signal(syscall.Signal(0))
  261. if err != nil {
  262. // Process is dead, remove file
  263. return os.Remove(infoFile)
  264. }
  265. // Process exists, but is it actually PizzaKV responding on that port?
  266. // Try to connect to the port
  267. addr := fmt.Sprintf("127.0.0.1:%d", info.Port)
  268. conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
  269. if err != nil {
  270. // Port is not responding, process might be stale or not PizzaKV
  271. // Remove the file and let user launch a new instance
  272. return os.Remove(infoFile)
  273. }
  274. conn.Close()
  275. // Process is still running and responding on the port
  276. return fmt.Errorf("pizzakv process (PID %d) is already running on port %d", info.PID, info.Port)
  277. }
  278. // KillExisting kills an existing pizzakv process based on the info file
  279. func KillExisting(infoFile string) error {
  280. data, err := os.ReadFile(infoFile)
  281. if err != nil {
  282. if os.IsNotExist(err) {
  283. return nil // No file, nothing to kill
  284. }
  285. return err
  286. }
  287. var info KVInfo
  288. if err := json.Unmarshal(data, &info); err != nil {
  289. // Invalid file, just remove it
  290. return os.Remove(infoFile)
  291. }
  292. // Try to kill the process
  293. process, err := os.FindProcess(info.PID)
  294. if err != nil {
  295. // Process doesn't exist, remove file
  296. return os.Remove(infoFile)
  297. }
  298. // Try SIGTERM first
  299. if err := process.Signal(syscall.SIGTERM); err == nil {
  300. // Wait a bit for graceful shutdown
  301. time.Sleep(1 * time.Second)
  302. // Check if still running
  303. if err := process.Signal(syscall.Signal(0)); err == nil {
  304. // Still running, force kill
  305. process.Kill()
  306. }
  307. } else {
  308. // SIGTERM failed, try SIGKILL
  309. process.Kill()
  310. }
  311. // Remove the info file
  312. return os.Remove(infoFile)
  313. }
  314. // ParsePort parses a port from a string (e.g., "localhost:8085" -> 8085)
  315. func ParsePort(addr string) (int, error) {
  316. parts := strings.Split(addr, ":")
  317. if len(parts) != 2 {
  318. return 0, fmt.Errorf("invalid address format: %s", addr)
  319. }
  320. port, err := strconv.Atoi(parts[1])
  321. if err != nil {
  322. return 0, fmt.Errorf("invalid port: %s", parts[1])
  323. }
  324. return port, nil
  325. }