4#if PEDIGREE_CHILD_WAIT_TESTS && THREADS
5#include "pedigree/kernel/Atomic.h"
6#include "pedigree/kernel/Log.h"
7#include "pedigree/kernel/process/Process.h"
8#include "pedigree/kernel/process/Scheduler.h"
9#include "pedigree/kernel/process/Semaphore.h"
10#include "pedigree/kernel/process/Thread.h"
11#include "pedigree/kernel/processor/Processor.h"
12#include "pedigree/kernel/processor/ProcessorInformation.h"
13#include "pedigree/kernel/time/Time.h"
14#include "pedigree/kernel/utilities/ZombieQueue.h"
17constexpr size_t TimeoutSeconds = 5;
18constexpr int ExitCode = 37;
20bool check(
bool condition,
const char* detail) {
22 ERROR(
"CHILD-WAIT-CORE: FAIL " << detail);
28 const auto start = Time::getTicks();
29 while (Time::getTicks() - start < TimeoutSeconds * Time::Multiplier::Second) {
30 if (process.getState() == state) {
44int stopEntry(
void* parameter) {
45 auto& context = *
static_cast<StopContext*
>(parameter);
47 for (
size_t i = 0; i < 2; ++i) {
49 context.ready.release();
50 if (!context.go.acquire(1, TimeoutSeconds)) {
51 context.failures += 1;
59 context.resumed.release();
64bool selection(
Process& parent,
Process& child,
bool stopped,
bool continued,
bool consume,
65 Process::ChildTransitionKind expected,
int signal = 0) {
68 transition.kind = Process::ChildTransitionKind::Stopped;
69 transition.stopSignal = -1;
71 return selected == (expected != Process::ChildTransitionKind::None) &&
72 transition.kind == expected && transition.stopSignal == signal;
75bool transitionSelection(
Process& parent) {
76 auto* child =
new Process(&parent,
true);
77 if (!check(child !=
nullptr,
"transition process allocation")) {
81 auto*
worker =
new Thread(child, stopEntry, &context,
nullptr,
false,
true,
true);
84 return check(
false,
"transition worker allocation");
86 worker->setName(
"child wait transition");
87 const bool started =
worker->start();
88 using Kind = Process::ChildTransitionKind;
89 bool passed = check(started,
"transition worker startup");
92 if (!check(context.ready.acquire(1, TimeoutSeconds),
"first stop entry")) {
96 if (!check(waitForState(*child, Process::Suspended),
"first stop publication")) {
99 bool ok = check(selection(parent, *child,
false,
false,
true, Kind::None) &&
100 selection(parent, *child,
false,
true,
true, Kind::None) &&
101 selection(parent, *child,
true,
false,
false, Kind::Stopped, 19) &&
102 selection(parent, *child,
true,
true,
false, Kind::Stopped, 19) &&
103 selection(parent, *child,
true,
false,
true, Kind::Stopped, 19) &&
104 selection(parent, *child,
true,
true,
true, Kind::None) &&
105 child->getState() == Process::Suspended,
106 "stop peek/consume or unmatched-class preservation");
108 if (!check(context.resumed.acquire(1, TimeoutSeconds) &&
109 context.ready.acquire(1, TimeoutSeconds),
110 "first resume and second stop entry")) {
113 ok &= check(selection(parent, *child,
true,
false,
true, Kind::None) &&
114 selection(parent, *child,
false,
true,
false, Kind::Continued) &&
115 selection(parent, *child,
true,
true,
false, Kind::Continued) &&
116 selection(parent, *child,
false,
true,
true, Kind::Continued) &&
117 selection(parent, *child,
true,
true,
false, Kind::None),
118 "continue peek/consume or unmatched-class preservation");
119 context.go.release();
120 if (!check(waitForState(*child, Process::Suspended),
"second stop publication")) {
123 ok &= check(selection(parent, *child,
true,
false,
false, Kind::Stopped, 20),
124 "second stop metadata");
126 if (!check(context.resumed.acquire(1, TimeoutSeconds),
"second resume")) {
129 ok &= check(selection(parent, *child,
true,
false,
true, Kind::None) &&
130 selection(parent, *child,
false,
true,
true, Kind::Continued) &&
131 selection(parent, *child,
true,
true,
false, Kind::None),
132 "resume did not replace the unconsumed stop");
136 context.abort =
true;
138 context.go.release();
142 if (!
worker->joinForCompletion()) {
143 FATAL(
"CHILD-WAIT-CORE: transition worker could not retire safely");
145 passed &= check(context.failures == 0,
"transition worker timed out");
148 NOTICE(
"CHILD-WAIT-CORE: PASS transition selection");
153struct TerminalContext {
156#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
163class ObservedProcess final :
public Process {
165 ObservedProcess(
Process& parent, TerminalContext& context)
166 :
Process(DeferredPublication(), &parent, true), m_Context(context) {
169 ~ObservedProcess()
override {
171 m_Context.destroyed += 1;
175 TerminalContext& m_Context;
178int exitEntry(
void* parameter) {
179 auto& context = *
static_cast<TerminalContext*
>(parameter);
180 context.ready.release();
181 const bool released = context.finish.acquire(1, TimeoutSeconds);
183 if (!process.
beginTermination(released ? ExitCode : 1) || !process.quiesceTermination()) {
184 FATAL(
"CHILD-WAIT-CORE: child failed to own its terminal teardown");
191#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
192TerminalContext* g_TerminalContext =
nullptr;
194void observeReaper(
Process* process, ZombieProcess::ReapPhase phase) {
195 auto* context = __atomic_load_n(&g_TerminalContext, __ATOMIC_ACQUIRE);
196 if (!context || context->target != process) {
199 if (phase == ZombieProcess::ReapPhase::Reapable) {
200 context->reapableCalls += 1;
205bool waitForObserverDrain(TerminalContext& context) {
206 const auto start = Time::getTicks();
207 while (Time::getTicks() - start < TimeoutSeconds * Time::Multiplier::Second) {
208 Thread* reaper = context.reaper;
211 info.channelOwner == context.target && reaper->
getStatus() == Thread::Sleeping) {
220bool terminalObserver(
Process& parent) {
221 TerminalContext context;
222 auto* child =
new ObservedProcess(parent, context);
223 if (!check(child !=
nullptr,
"terminal process allocation")) {
226 auto*
worker =
new Thread(child, exitEntry, &context,
nullptr,
false,
true,
true);
229 return check(
false,
"terminal worker allocation");
231 worker->setName(
"child wait terminal");
234 if (!
worker->joinForCompletion()) {
235 FATAL(
"CHILD-WAIT-CORE: failed terminal startup could not retire safely");
238 return check(
false,
"terminal worker startup");
240 bool passed = check(context.ready.acquire(1, TimeoutSeconds),
"terminal worker entry");
241 context.finish.release();
242 if (!waitForState(*child, Process::Terminated)) {
243 FATAL(
"CHILD-WAIT-CORE: terminal publication timed out");
246 const size_t pid = child->
getId();
247 const auto userBefore = parent.getReapedChildrenUserTime();
248 const auto kernelBefore = parent.getReapedChildrenKernelTime();
252 passed &= check(child->getState() == Process::Terminated &&
254 "terminal observer admission under the child-state guard");
257 FATAL(
"CHILD-WAIT-CORE: terminal observer could not retain an off-stack child");
260 const auto childUser = observer->
getUserTime() + observer->getReapedChildrenUserTime();
261 const auto childKernel = observer->getKernelTime() + observer->getReapedChildrenKernelTime();
262 passed &= check(status == (ExitCode << 8) && observer->getState() == Process::Terminated &&
263 parent.getReapedChildrenUserTime() == userBefore &&
264 parent.getReapedChildrenKernelTime() == kernelBefore,
265 "terminal observation consumed status or accounted CPU time");
272 FATAL(
"CHILD-WAIT-CORE: terminal observer prevented the sole reaper claim");
276 passed &= check(!duplicate,
"a competing consumer claimed the child twice");
278 Time::Timestamp user = 0, kernel = 0;
280 passed &= check(user == childUser && kernel == childKernel &&
281 parent.getReapedChildrenUserTime() == userBefore + childUser &&
282 parent.getReapedChildrenKernelTime() == kernelBefore + childKernel,
283 "sole consuming claim did not account the final CPU totals once");
284#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
285 context.target = child;
286 __atomic_store_n(&g_TerminalContext, &context, __ATOMIC_RELEASE);
287 ZombieProcess::setReapHook(observeReaper);
291#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
292 passed &= check(waitForObserverDrain(context) && context.reapableCalls == 1 && !context.destroyed,
293 "published reaper did not wait for the retained observer before destruction");
296 "removed child admitted a new observer");
300 NOTICE(
"CHILD-WAIT-CORE: SKIP exact reaper wait (hosted hook only)");
304 parent.getReapedChildrenUserTime() == userBefore + childUser &&
305 parent.getReapedChildrenKernelTime() == kernelBefore + childKernel,
306 "retained terminal snapshot or once-only accounting changed after publication");
310 const auto releasedAt = Time::getTicks();
311 while (!context.destroyed &&
312 Time::getTicks() - releasedAt < TimeoutSeconds * Time::Multiplier::Second) {
315 if (!context.destroyed) {
316 FATAL(
"CHILD-WAIT-CORE: observer release did not unblock destruction");
318 const bool drained = ZombieQueue::instance().
drain();
319#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
320 ZombieProcess::setReapHook(
nullptr);
321 __atomic_store_n(&g_TerminalContext,
static_cast<TerminalContext*
>(
nullptr), __ATOMIC_RELEASE);
324 FATAL(
"CHILD-WAIT-CORE: child destruction queue did not drain safely");
327 check(context.destroyed == 1 && parent.getReapedChildrenUserTime() == userBefore + user &&
328 parent.getReapedChildrenKernelTime() == kernelBefore + kernel,
329 "observer release did not complete exactly one destruction");
331 NOTICE(
"CHILD-WAIT-CORE: PASS terminal observer and sole reaper");
337bool runChildWaitRegressions() {
338 NOTICE(
"CHILD-WAIT-CORE: BEGIN");
340 if (!check(parent !=
nullptr,
"isolated parent allocation")) {
343 const bool passed = transitionSelection(*parent) && terminalObserver(*parent);
346 NOTICE(
"CHILD-WAIT-CORE: END PASS");
void setExitStatus(int code)
bool beginTermination(int code=0, Subsystem::ExitCause cause=Subsystem::ExitCause::Normal)
@ Reaped
Terminal wait status is visible; the owner may still be on-stack.
void accountReapedChild(const Process *child, Time::Timestamp &user, Time::Timestamp &kernel)
WaitQueue::Guard acquireChildStateWait()
bool selectPendingChildTransition(bool includeStopped, bool includeContinued, bool consume, ChildTransition &transition)
void suspendIfContinuationEpoch(int stopSignal, size_t continuationEpoch)
bool waitUntilTerminationReapable()
Time::Timestamp getUserTime() const
void prepareForDestruction()
ReaperClaim tryClaimReaper()
size_t getContinuationEpoch()
void finishTermination(bool notifyParent=false) NORETURN
static ProcessorInformation & information()
static Scheduler & instance()
MUST_USE_RESULT bool acquireProcess(ProcessLease &lease, size_t n)
@ TerminateThread
Exit only this thread during Process exit.
bool getWaitDebugInfo(WaitDebugInfo &info)