The Pedigree Project 0.1
input-regressions.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/Atomic.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/machine/InputManager.h"
11#include "pedigree/kernel/process/Scheduler.h"
12#include "pedigree/kernel/process/Thread.h"
13
14namespace {
15constexpr size_t Attempts = 10000;
16
17bool check(bool condition, const char* detail) {
18 if (condition) {
19 return true;
20 }
21
22 ERROR("HOSTED-WAIT-TEST: FAIL input-callback-lifetime: " << detail);
23 return false;
24}
25
26struct CallbackLifetimeContext {
27 CallbackLifetimeContext()
28 : removers{nullptr, nullptr},
29 phase(0),
30 removersStarted(0),
31 hookCalls(0),
32 hookObservedDrain(0),
33 callbackCalls(0),
34 callbackAfterRemoval(0),
35 removalReturned(0),
36 failures(0) {}
37
38 Thread* removers[2];
39 Atomic<size_t> phase;
40 Atomic<size_t> removersStarted;
41 Atomic<size_t> hookCalls;
42 Atomic<size_t> hookObservedDrain;
43 Atomic<size_t> callbackCalls;
44 Atomic<size_t> callbackAfterRemoval;
45 Atomic<size_t> removalReturned;
46 Atomic<size_t> failures;
47};
48
49CallbackLifetimeContext* g_CallbackLifetimeContext = nullptr;
50
51void lifetimeCallback(InputManager::InputNotification& notification) {
52 CallbackLifetimeContext* context = reinterpret_cast<CallbackLifetimeContext*>(notification.meta);
53 context->callbackCalls += 1;
54 if (context->removalReturned) {
55 context->callbackAfterRemoval += 1;
56 }
57}
58
59void callbackPinHook(InputManager::callback_t callback, void* meta) {
60 CallbackLifetimeContext* context = g_CallbackLifetimeContext;
61 if (!context || callback != lifetimeCallback || meta != context ||
62 !context->phase.compareAndSwap(0, 1)) {
63 return;
64 }
65
66 context->hookCalls += 1;
67 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
68 bool allDraining = context->removersStarted == 2;
69 for (size_t i = 0; allDraining && i < 2; ++i) {
70 Thread::WaitDebugInfo info = {};
71 uintptr_t debugAddress = 0;
72 allDraining &= context->removers[i]->getWaitDebugInfo(info) && info.queue &&
73 info.channelOwner && info.queued &&
74 context->removers[i]->getDebugState(debugAddress) == Thread::CallbackDrain &&
75 debugAddress == reinterpret_cast<uintptr_t>(lifetimeCallback);
76 }
77 if (allDraining) {
78 context->hookObservedDrain += 1;
79 break;
80 }
82 }
83
84 if (!context->hookObservedDrain) {
85 context->failures += 1;
86 }
87 context->phase = 2;
88}
89
90int removePinnedCallback(void* parameter) {
91 CallbackLifetimeContext* context = reinterpret_cast<CallbackLifetimeContext*>(parameter);
92 for (size_t attempt = 0; attempt < Attempts && context->phase != static_cast<size_t>(1);
93 ++attempt) {
95 }
96
97 if (context->phase != static_cast<size_t>(1)) {
98 context->failures += 1;
99 return 1;
100 }
101
102 context->removersStarted += 1;
103 InputManager::instance().removeCallback(lifetimeCallback, context);
104 context->removalReturned += 1;
105 return 0;
106}
107
108struct SelfRemovalContext {
109 SelfRemovalContext() : calls(0) {}
110
111 Atomic<size_t> calls;
112};
113
114void selfRemovingCallback(InputManager::InputNotification& notification) {
115 SelfRemovalContext* context = reinterpret_cast<SelfRemovalContext*>(notification.meta);
116 context->calls += 1;
117 InputManager::instance().removeCallback(selfRemovingCallback, context);
118}
119
120bool callbackLifetimeBarrier() {
122 CallbackLifetimeContext context;
123 for (size_t i = 0; i < 2; ++i) {
124 context.removers[i] = new Thread(Scheduler::instance().getKernelProcess(), removePinnedCallback,
125 &context, nullptr, false, true);
126 context.removers[i]->setName("hosted input-callback remover");
127 }
128
129 manager.installCallback(InputManager::Key, lifetimeCallback, &context);
130 g_CallbackLifetimeContext = &context;
131 InputManager::setCallbackPinHook(callbackPinHook);
132 manager.keyPressed('i');
133
134 bool joined = true;
135 for (auto remover : context.removers) {
136 joined &= remover->join();
137 }
138 InputManager::setCallbackPinHook(nullptr);
139 g_CallbackLifetimeContext = nullptr;
140
141 // Failed setup must not leave a module callback in the core registry.
142 manager.removeCallback(lifetimeCallback, &context);
143
144 for (size_t attempt = 0; attempt < 100; ++attempt) {
146 }
147 const size_t callsAfterRemoval = context.callbackCalls;
148 manager.keyPressed('j');
149 for (size_t attempt = 0; attempt < 100; ++attempt) {
151 }
152
153 SelfRemovalContext selfRemoval;
154 manager.installCallback(InputManager::Key, selfRemovingCallback, &selfRemoval);
155 manager.keyPressed('k');
156 for (size_t attempt = 0; attempt < Attempts && !selfRemoval.calls; ++attempt) {
158 }
159 const size_t callsAfterSelfRemoval = selfRemoval.calls;
160 manager.keyPressed('l');
161 for (size_t attempt = 0; attempt < 100; ++attempt) {
163 }
164 manager.removeCallback(selfRemovingCallback, &selfRemoval);
165
166 bool passed = true;
167 passed &= check(joined && context.failures == 0,
168 "the concurrent removal worker did not complete cleanly");
169 passed &= check(context.hookCalls == 1 && context.hookObservedDrain == 1,
170 "removal did not wait on the committed callback invocation");
171 passed &= check(context.callbackCalls == 1 && context.callbackCalls == callsAfterRemoval &&
172 context.callbackAfterRemoval == 0 && context.removalReturned == 2,
173 "a callback began after removeCallback returned");
174 passed &= check(callsAfterSelfRemoval == 1 && selfRemoval.calls == 1,
175 "self-removal did not retire the callback after its active invocation");
176
177 if (passed) {
178 NOTICE("HOSTED-WAIT-TEST: PASS input-callback-lifetime");
179 }
180 return passed;
181}
182} // namespace
183
184bool runHostedInputRegressions() {
185 return callbackLifetimeBarrier();
186}
static const int Key
static InputManager & instance()
Singleton design.
void installCallback(CallbackType filter, callback_t callback, void *meta=0, Thread *pThread=0, uintptr_t param=0)
Installs a callback.
void(* callback_t)(InputNotification &)
Callback function type.
void removeCallback(callback_t callback, void *meta=0, Thread *pThread=0)
Removes a callback.
void keyPressed(uint64_t key)
Called whenever a key is pressed and needs to be added to the queue.
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226