1package testhelper
2
3import (
4	"fmt"
5	"os"
6	"os/exec"
7	"strings"
8	"syscall"
9	"time"
10
11	"gitlab.com/gitlab-org/gitaly/v14/internal/command/commandcounter"
12	"gitlab.com/gitlab-org/gitaly/v14/internal/helper/text"
13	"go.uber.org/goleak"
14)
15
16// mustHaveNoGoroutines panics if it finds any Goroutines running.
17func mustHaveNoGoroutines() {
18	if err := goleak.Find(
19		// opencensus has a "defaultWorker" which is started by the package's
20		// `init()` function. There is no way to stop this worker, so it will leak
21		// whenever we import the package.
22		goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
23		// The Ruby server's load balancer is registered in the `init()` function
24		// of our "rubyserver/balancer" package. Ideally we'd clean this up
25		// eventually, but the pragmatic approach is to just wait until we remove
26		// the Ruby sidecar altogether.
27		goleak.IgnoreTopFunction("google.golang.org/grpc.(*ccBalancerWrapper).watcher"),
28		goleak.IgnoreTopFunction("gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/rubyserver/balancer.(*builder).monitor"),
29		// labkit's logger spawns a Goroutine which cannot be closed when calling
30		// `Initialize()`.
31		goleak.IgnoreTopFunction("gitlab.com/gitlab-org/labkit/log.listenForSignalHangup"),
32		// The backchannel code is somehow stock on closing its connections. I have no clue
33		// why that is, but we should investigate.
34		goleak.IgnoreTopFunction("gitlab.com/gitlab-org/gitaly/v14/internal/backchannel.clientHandshake.serve.func4"),
35	); err != nil {
36		panic(fmt.Errorf("goroutines running: %w", err))
37	}
38}
39
40// mustHaveNoChildProcess panics if it finds a running or finished child
41// process. It waits for 2 seconds for processes to be cleaned up by other
42// goroutines.
43func mustHaveNoChildProcess() {
44	waitDone := make(chan struct{})
45	go func() {
46		commandcounter.WaitAllDone()
47		close(waitDone)
48	}()
49
50	select {
51	case <-waitDone:
52	case <-time.After(2 * time.Second):
53	}
54
55	if err := mustFindNoFinishedChildProcess(); err != nil {
56		panic(err)
57	}
58
59	if err := mustFindNoRunningChildProcess(); err != nil {
60		panic(err)
61	}
62}
63
64func mustFindNoFinishedChildProcess() error {
65	// Wait4(pid int, wstatus *WaitStatus, options int, rusage *Rusage) (wpid int, err error)
66	//
67	// We use pid -1 to wait for any child. We don't care about wstatus or
68	// rusage. Use WNOHANG to return immediately if there is no child waiting
69	// to be reaped.
70	wpid, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil)
71	if err == nil && wpid > 0 {
72		return fmt.Errorf("wait4 found child process %d", wpid)
73	}
74
75	return nil
76}
77
78func mustFindNoRunningChildProcess() error {
79	pgrep := exec.Command("pgrep", "-P", fmt.Sprintf("%d", os.Getpid()))
80	desc := fmt.Sprintf("%q", strings.Join(pgrep.Args, " "))
81
82	out, err := pgrep.Output()
83	if err == nil {
84		pidsComma := strings.Replace(text.ChompBytes(out), "\n", ",", -1)
85		psOut, _ := exec.Command("ps", "-o", "pid,args", "-p", pidsComma).Output()
86		return fmt.Errorf("found running child processes %s:\n%s", pidsComma, psOut)
87	}
88
89	exitError, ok := err.(*exec.ExitError)
90	if !ok {
91		return fmt.Errorf("expected ExitError, got %T", err)
92	}
93
94	if exitError.ExitCode() == 1 {
95		return nil
96	}
97
98	return fmt.Errorf("%s: %w", desc, err)
99}
100