1package telegram
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strconv"
9 "strings"
10 "syscall"
11 "time"
12)
13
14// PIDPath returns the location of the bot's pid file.
15func PIDPath(zotHome string) string {
16 return filepath.Join(zotHome, "bot.pid")
17}
18
19// LogPath returns the location of the bot's log file (stdout+stderr
20// from a detached `zot bot start`).
21func LogPath(zotHome string) string {
22 return filepath.Join(zotHome, "logs", "bot.log")
23}
24
25// WritePID persists pid to bot.pid. Overwrites any existing file.
26func WritePID(zotHome string, pid int) error {
27 p := PIDPath(zotHome)
28 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
29 return err
30 }
31 return os.WriteFile(p, []byte(strconv.Itoa(pid)+"\n"), 0o644)
32}
33
34// ReadPID returns the pid stored in bot.pid, or 0 if the file doesn't
35// exist. Returns an error for any other read/parse failure.
36func ReadPID(zotHome string) (int, error) {
37 b, err := os.ReadFile(PIDPath(zotHome))
38 if errors.Is(err, os.ErrNotExist) {
39 return 0, nil
40 }
41 if err != nil {
42 return 0, err
43 }
44 pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
45 if err != nil {
46 return 0, fmt.Errorf("parse pid: %w", err)
47 }
48 return pid, nil
49}
50
51// RemovePID deletes the pid file if it exists.
52func RemovePID(zotHome string) error {
53 err := os.Remove(PIDPath(zotHome))
54 if errors.Is(err, os.ErrNotExist) {
55 return nil
56 }
57 return err
58}
59
60// IsRunning returns (pid, true) if a live process with the recorded
61// pid exists, or (pid, false) if the pid file points to a dead process.
62// Stale pid files are left in place; the caller may remove them.
63func IsRunning(zotHome string) (int, bool, error) {
64 pid, err := ReadPID(zotHome)
65 if err != nil {
66 return 0, false, err
67 }
68 if pid <= 0 {
69 return 0, false, nil
70 }
71 proc, err := os.FindProcess(pid)
72 if err != nil {
73 return pid, false, nil
74 }
75 // signal 0 is POSIX's "does the process exist?" probe. On Windows
76 // os.Process is always usable and Signal(0) returns nil, so we'd
77 // miss stale pids; acceptable for the macos/linux-first audience.
78 if err := proc.Signal(syscall.Signal(0)); err != nil {
79 if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
80 return pid, false, nil
81 }
82 // Other errors (EPERM) mean the process exists but we can't
83 // inspect it; treat as running.
84 return pid, true, nil
85 }
86 return pid, true, nil
87}
88
89// StopProcess sends SIGTERM to pid and waits up to graceful for it to
90// exit, then escalates to SIGKILL. Returns nil if the process is gone.
91func StopProcess(pid int, graceful time.Duration) error {
92 proc, err := os.FindProcess(pid)
93 if err != nil {
94 return err
95 }
96 _ = proc.Signal(syscall.SIGTERM)
97
98 deadline := time.Now().Add(graceful)
99 for time.Now().Before(deadline) {
100 if err := proc.Signal(syscall.Signal(0)); err != nil {
101 if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
102 return nil
103 }
104 }
105 time.Sleep(100 * time.Millisecond)
106 }
107 _ = proc.Kill()
108 return nil
109}