| 1 | package activator |
| 2 | |
| 3 | import ( |
| 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. |
| 32 | func 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". |
| 64 | func 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. |
| 74 | func 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. |
| 101 | func 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. |
| 108 | func 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 | |
| 128 | func 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 | |