v0.30.0 c899179

run_linux.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//go:build linux
4
5package gitremote
6
7import (
8	"context"
9	"errors"
10	"fmt"
11	"os/exec"
12	"syscall"
13
14	"golang.org/x/sys/unix"
15)
16
17// Tests replace these two.
18var (
19	waitid    = unix.Waitid
20	killGroup = func(pgid int) error { return syscall.Kill(-pgid, syscall.SIGKILL) }
21)
22
23// run starts cmd, waits for it, and kills what it leaves running.
24//
25// git runs git-remote-https, which is the process that connects and that holds
26// the pipes of git's output. Killing git alone leaves it running, and Wait
27// blocked on the pipes. So git leads a process group of its own, and the group
28// is killed when ctx is done and also when git has exited.
29//
30// The invariant that makes killing a group safe: the group is only signalled
31// while its leader is still not reaped. Linux does not reuse a PID as long as
32// the process is a zombie nobody waited for, nor while it is the group ID of a
33// live member, so -pid is still our group and cannot be another one. The
34// leader is therefore waited for with waitid(WNOWAIT), which observes its exit
35// without reaping it; the group is signalled; and only then cmd.Wait reaps. A
36// kill(-pid) after Wait, or from Cmd.Cancel which races with it, would not be
37// safe. When waitid fails, whatever the reason, nothing proves the leader is
38// unreaped: the group is not signalled, only the leader is killed (through
39// its pidfd, which cannot hit a reused PID), and the error is returned.
40// cmd.WaitDelay (set by the caller) stays the backstop for a descendant that
41// escaped the group and holds the pipes.
42func run(ctx context.Context, cmd *exec.Cmd) error {
43	if err := ctx.Err(); err != nil {
44		return err
45	}
46	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
47	if err := cmd.Start(); err != nil {
48		return err
49	}
50	pid := cmd.Process.Pid
51	observed := make(chan error, 1)
52	go func() { observed <- waitExit(pid) }()
53
54	var werr error
55	select {
56	case err := <-observed:
57		if err != nil {
58			werr = err // the leader may have been reaped: no group signal
59		} else {
60			signalGroup(cmd, pid) // exited, not reaped: a zombie
61		}
62	case <-ctx.Done():
63		select {
64		case err := <-observed:
65			// waitid answered at the same time: trust it as above.
66			if err != nil {
67				werr = err
68			} else {
69				signalGroup(cmd, pid)
70			}
71		default:
72			// No waitid error so far: the leader is alive or a zombie, unreaped
73			// (only cmd.Wait below reaps it).
74			signalGroup(cmd, pid)
75			if err := <-observed; err != nil {
76				werr = err // after the signals: reported, nothing more to do
77			}
78		}
79	}
80	if werr != nil {
81		_ = cmd.Process.Kill()
82	}
83	err := cmd.Wait()
84	if werr != nil {
85		return fmt.Errorf("waiting for the process %d: %w", pid, werr)
86	}
87	return err
88}
89
90// signalGroup kills the group of the leader, and the leader alone should it
91// have left the group. The leader must not be reaped yet.
92func signalGroup(cmd *exec.Cmd, pid int) {
93	_ = killGroup(pid)
94	_ = cmd.Process.Kill()
95}
96
97// waitExit returns when the child pid has exited, without reaping it, or the
98// error of waitid.
99func waitExit(pid int) error {
100	var info unix.Siginfo
101	for {
102		err := waitid(unix.P_PID, pid, &info, unix.WEXITED|unix.WNOWAIT, nil)
103		if !errors.Is(err, unix.EINTR) {
104			return err
105		}
106	}
107}

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 2

Tags 45

↑↓ moveenter openesc close