carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "bytes"
5 "context"
6 "os"
7 "path/filepath"
8 "strconv"
9 "time"
10)
11
12// The fake apps these tests launch (stageRecordingLive, stageHelperApp and
13// their kin) record what they were handed to a file and exit. They never
14// bind their socket, so a Start against one waits out its whole health
15// context, and a test that wants to see that NO app ran used to wait out a
16// fixed timeout for a file that was never coming. Both waits had a real
17// condition behind them, and the helpers here wait for that instead:
18//
19// - the app has recorded: its file exists AND it has exited, so whatever
20// it was going to write is written;
21// - nothing can still record: no child of this test binary is running,
22// so no process exists that could write the file later.
23//
24// Both read /proc, not the code under test, so they rest on what the kernel
25// says ran, never on the launch's own account of itself. Where /proc cannot
26// answer, or an unrelated child of this binary is still running, they fall
27// back to the original bound, which only makes them slower.
28
29// runningChildren reports how many children of this test binary are still
30// running. A zombie (exited, not yet reaped) is not running: it has written
31// all it ever will. ok is false when /proc cannot be read.
32func runningChildren() (n int, ok bool) {
33 entries, err := os.ReadDir("/proc")
34 if err != nil {
35 return 0, false
36 }
37 self := strconv.Itoa(os.Getpid())
38 for _, e := range entries {
39 name := e.Name()
40 if name == "" || name[0] < '0' || name[0] > '9' {
41 continue
42 }
43 raw, err := os.ReadFile(filepath.Join("/proc", name, "stat"))
44 if err != nil {
45 continue // gone since the listing
46 }
47 // "pid (comm) state ppid ...": comm may hold spaces and parens.
48 i := bytes.LastIndexByte(raw, ')')
49 if i < 0 {
50 continue
51 }
52 f := bytes.Fields(raw[i+1:])
53 if len(f) < 2 || string(f[1]) != self {
54 continue
55 }
56 if s := string(f[0]); s != "Z" && s != "X" {
57 n++
58 }
59 }
60 return n, true
61}
62
63// noChildRunning is runningChildren's "none, for certain".
64func noChildRunning() bool {
65 n, ok := runningChildren()
66 return ok && n == 0
67}
68
69// awaitChildFile returns path's contents once it exists, or "" once no
70// child of this test binary is running, so nothing is left to write it,
71// or once within has passed (the old fixed wait, still the bound). Call it
72// only after the launch under test has returned: a launch still in flight
73// could spawn after the check.
74func awaitChildFile(path string, within time.Duration) string {
75 deadline := time.Now().Add(within)
76 for {
77 if raw, err := os.ReadFile(path); err == nil {
78 return string(raw)
79 }
80 if noChildRunning() {
81 // A child that exited between the read and the check wrote its
82 // file before it exited.
83 if raw, err := os.ReadFile(path); err == nil {
84 return string(raw)
85 }
86 return ""
87 }
88 if !time.Now().Before(deadline) {
89 return ""
90 }
91 time.Sleep(5 * time.Millisecond)
92 }
93}
94
95// recordedCtx is the bounded context a fake-app test hands Start, also
96// cancelled the moment the fake app has recorded: every path in paths
97// exists and no child of this test binary is still running. The health
98// wait the app never answers has nothing left to show after that; before,
99// the test sat out the whole timeout. Start then fails on the cancel
100// rather than the deadline, which these tests ignore.
101func recordedCtx(within time.Duration, paths ...string) (context.Context, context.CancelFunc) {
102 return ctxUntil(within, func() bool { return allExist(paths) && noChildRunning() })
103}
104
105// ctxUntil is a context bounded by within and also cancelled once done
106// reports true (polled every 5 ms): the condition a test was really waiting
107// for when it sat out the timeout.
108func ctxUntil(within time.Duration, done func() bool) (context.Context, context.CancelFunc) {
109 ctx, cancel := context.WithTimeout(context.Background(), within)
110 go func() {
111 t := time.NewTicker(5 * time.Millisecond)
112 defer t.Stop()
113 for {
114 select {
115 case <-ctx.Done():
116 return
117 case <-t.C:
118 }
119 if done() {
120 cancel()
121 return
122 }
123 }
124 }()
125 return ctx, cancel
126}
127
128func allExist(paths []string) bool {
129 for _, p := range paths {
130 if _, err := os.Stat(p); err != nil {
131 return false
132 }
133 }
134 return true
135}
136