package activator import ( "bytes" "context" "os" "path/filepath" "strconv" "time" ) // The fake apps these tests launch (stageRecordingLive, stageHelperApp and // their kin) record what they were handed to a file and exit. They never // bind their socket, so a Start against one waits out its whole health // context, and a test that wants to see that NO app ran used to wait out a // fixed timeout for a file that was never coming. Both waits had a real // condition behind them, and the helpers here wait for that instead: // // - the app has recorded: its file exists AND it has exited, so whatever // it was going to write is written; // - nothing can still record: no child of this test binary is running, // so no process exists that could write the file later. // // Both read /proc, not the code under test, so they rest on what the kernel // says ran, never on the launch's own account of itself. Where /proc cannot // answer, or an unrelated child of this binary is still running, they fall // back to the original bound, which only makes them slower. // runningChildren reports how many children of this test binary are still // running. A zombie (exited, not yet reaped) is not running: it has written // all it ever will. ok is false when /proc cannot be read. func runningChildren() (n int, ok bool) { entries, err := os.ReadDir("/proc") if err != nil { return 0, false } self := strconv.Itoa(os.Getpid()) for _, e := range entries { name := e.Name() if name == "" || name[0] < '0' || name[0] > '9' { continue } raw, err := os.ReadFile(filepath.Join("/proc", name, "stat")) if err != nil { continue // gone since the listing } // "pid (comm) state ppid ...": comm may hold spaces and parens. i := bytes.LastIndexByte(raw, ')') if i < 0 { continue } f := bytes.Fields(raw[i+1:]) if len(f) < 2 || string(f[1]) != self { continue } if s := string(f[0]); s != "Z" && s != "X" { n++ } } return n, true } // noChildRunning is runningChildren's "none, for certain". func noChildRunning() bool { n, ok := runningChildren() return ok && n == 0 } // awaitChildFile returns path's contents once it exists, or "" once no // child of this test binary is running, so nothing is left to write it, // or once within has passed (the old fixed wait, still the bound). Call it // only after the launch under test has returned: a launch still in flight // could spawn after the check. func awaitChildFile(path string, within time.Duration) string { deadline := time.Now().Add(within) for { if raw, err := os.ReadFile(path); err == nil { return string(raw) } if noChildRunning() { // A child that exited between the read and the check wrote its // file before it exited. if raw, err := os.ReadFile(path); err == nil { return string(raw) } return "" } if !time.Now().Before(deadline) { return "" } time.Sleep(5 * time.Millisecond) } } // recordedCtx is the bounded context a fake-app test hands Start, also // cancelled the moment the fake app has recorded: every path in paths // exists and no child of this test binary is still running. The health // wait the app never answers has nothing left to show after that; before, // the test sat out the whole timeout. Start then fails on the cancel // rather than the deadline, which these tests ignore. func recordedCtx(within time.Duration, paths ...string) (context.Context, context.CancelFunc) { return ctxUntil(within, func() bool { return allExist(paths) && noChildRunning() }) } // ctxUntil is a context bounded by within and also cancelled once done // reports true (polled every 5 ms): the condition a test was really waiting // for when it sat out the timeout. func ctxUntil(within time.Duration, done func() bool) (context.Context, context.CancelFunc) { ctx, cancel := context.WithTimeout(context.Background(), within) go func() { t := time.NewTicker(5 * time.Millisecond) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: } if done() { cancel() return } } }() return ctx, cancel } func allExist(paths []string) bool { for _, p := range paths { if _, err := os.Stat(p); err != nil { return false } } return true }