1package app
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strconv"
9 "syscall"
10
11 "github.com/bnema/zerowrap"
12)
13
14// SendReloadSignal sends SIGUSR1 to the running Gordon process.
15func SendReloadSignal() error {
16 process, _, err := findRunningProcess()
17 if err != nil {
18 return err
19 }
20
21 if err := process.Signal(syscall.SIGUSR1); err != nil {
22 return fmt.Errorf("failed to send reload signal: %w", err)
23 }
24
25 return nil
26}
27
28// createPidFile creates a PID file for the Gordon process.
29// SECURITY: Prefers secure locations (XDG_RUNTIME_DIR, ~/.gordon/run) over /tmp
30// to prevent symlink attacks and unauthorized access.
31func createPidFile(log zerowrap.Logger) string {
32 pid := os.Getpid()
33
34 // SECURITY: Prioritize secure locations over /tmp
35 var locations []string
36
37 // Try secure runtime directory first
38 if runtimeDir, err := getSecureRuntimeDir(); err == nil {
39 locations = append(locations, filepath.Join(runtimeDir, "gordon.pid"))
40 }
41
42 // Fall back to home directory
43 if homeDir, err := os.UserHomeDir(); err == nil {
44 locations = append(locations, filepath.Join(homeDir, ".gordon", "gordon.pid"))
45 }
46
47 // Last resort: /tmp (least secure due to world-writable)
48 locations = append(locations, filepath.Join(os.TempDir(), "gordon.pid"))
49
50 for _, location := range locations {
51 // Ensure parent directory exists with secure permissions
52 if err := os.MkdirAll(filepath.Dir(location), 0700); err != nil {
53 continue
54 }
55 if err := os.WriteFile(location, fmt.Appendf(nil, "%d", pid), 0600); err == nil {
56 log.Debug().Str("pid_file", location).Int("pid", pid).Msg("created PID file")
57 return location
58 }
59 }
60
61 log.Warn().Int("pid", pid).Msg("failed to create PID file in any location")
62 return ""
63}
64
65// removePidFile removes the PID file.
66func removePidFile(pidFile string, log zerowrap.Logger) {
67 if err := os.Remove(pidFile); err != nil {
68 log.Warn().Err(err).Str("pid_file", pidFile).Msg("failed to remove PID file")
69 } else {
70 log.Debug().Str("pid_file", pidFile).Msg("removed PID file")
71 }
72}
73
74func pidFileLocations() []string {
75 var locations []string
76 seen := make(map[string]struct{})
77 add := func(path string) {
78 if path == "" {
79 return
80 }
81 if _, ok := seen[path]; ok {
82 return
83 }
84 seen[path] = struct{}{}
85 locations = append(locations, path)
86 }
87
88 // Check secure runtime directory first
89 if runtimeDir, err := getSecureRuntimeDir(); err == nil {
90 add(filepath.Join(runtimeDir, "gordon.pid"))
91 }
92
93 // Also check canonical /run/user/<uid> runtime path. This handles cases where
94 // Gordon started under systemd user services with runtime dir available, but
95 // CLI invocations (e.g. non-interactive SSH) don't have XDG_RUNTIME_DIR set.
96 runtimeByUID := filepath.Join("/run/user", strconv.Itoa(os.Getuid()), "gordon", "gordon.pid")
97 add(runtimeByUID)
98
99 // Check explicit XDG_RUNTIME_DIR if present in this process env.
100 if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" {
101 add(filepath.Join(runtimeDir, "gordon.pid"))
102 }
103
104 // Check home directory
105 if homeDir, err := os.UserHomeDir(); err == nil {
106 add(filepath.Join(homeDir, ".gordon", "gordon.pid"))
107 // Legacy location for backward compatibility
108 add(filepath.Join(homeDir, ".gordon.pid"))
109 }
110
111 // Legacy /tmp locations for backward compatibility
112 add(filepath.Join(os.TempDir(), "gordon.pid"))
113 add("/tmp/gordon.pid")
114
115 return locations
116}
117
118// findRunningPidFile returns the first PID file whose PID belongs to a live process.
119// Stale/invalid PID files are ignored and removed when possible.
120func findRunningPidFile() (string, int, error) {
121 return findRunningPidFileInLocations(pidFileLocations())
122}
123
124func findRunningPidFileInLocations(locations []string) (string, int, error) {
125 foundAny := false
126
127 for _, location := range locations {
128 pidBytes, err := os.ReadFile(location)
129 if err != nil {
130 continue
131 }
132
133 foundAny = true
134
135 var pid int
136 if _, err := fmt.Sscanf(string(pidBytes), "%d", &pid); err != nil || pid <= 0 {
137 _ = os.Remove(location)
138 continue
139 }
140
141 if isProcessAlive(pid) {
142 return location, pid, nil
143 }
144
145 _ = os.Remove(location)
146 }
147
148 if foundAny {
149 return "", 0, fmt.Errorf("found stale gordon PID file(s), is Gordon running?")
150 }
151
152 return "", 0, fmt.Errorf("gordon PID file not found, is Gordon running?")
153}
154
155func findRunningProcess() (*os.Process, int, error) {
156 _, pid, err := findRunningPidFile()
157 if err != nil {
158 return nil, 0, err
159 }
160
161 process, err := os.FindProcess(pid)
162 if err != nil {
163 return nil, 0, fmt.Errorf("failed to find process: %w", err)
164 }
165
166 return process, pid, nil
167}
168
169func isProcessAlive(pid int) bool {
170 process, err := os.FindProcess(pid)
171 if err != nil {
172 return false
173 }
174
175 err = process.Signal(syscall.Signal(0))
176 if err == nil {
177 return true
178 }
179
180 return errors.Is(err, syscall.EPERM)
181}