v3.3.1 31f1eec

pidfile.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close