/src/FreeRDP/libfreerdp/core/timer.c
Line | Count | Source (jump to first uncovered line) |
1 | | /** |
2 | | * FreeRDP: A Remote Desktop Protocol Implementation |
3 | | * Timer implementation |
4 | | * |
5 | | * Copyright 2025 Armin Novak <anovak@thincast.com> |
6 | | * Copyright 2025 Thincast Technologies GmbH |
7 | | * |
8 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
9 | | * you may not use this file except in compliance with the License. |
10 | | * You may obtain a copy of the License at |
11 | | * |
12 | | * http://www.apache.org/licenses/LICENSE-2.0 |
13 | | * |
14 | | * Unless required by applicable law or agreed to in writing, software |
15 | | * distributed under the License is distributed on an "AS IS" BASIS, |
16 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
17 | | * See the License for the specific language governing permissions and |
18 | | * limitations under the License. |
19 | | */ |
20 | | |
21 | | #include <winpr/thread.h> |
22 | | #include <winpr/collections.h> |
23 | | |
24 | | #include <freerdp/timer.h> |
25 | | #include <freerdp/log.h> |
26 | | #include "rdp.h" |
27 | | #include "utils.h" |
28 | | #include "timer.h" |
29 | | |
30 | | #if !defined(EMSCRIPTEN) |
31 | | #define FREERDP_TIMER_SUPPORTED |
32 | | #else |
33 | | #define TAG FREERDP_TAG("timer") |
34 | | #endif |
35 | | |
36 | | typedef ALIGN64 struct |
37 | | { |
38 | | FreeRDP_TimerID id; |
39 | | uint64_t intervallNS; |
40 | | uint64_t nextRunTimeNS; |
41 | | FreeRDP_TimerCallback cb; |
42 | | void* userdata; |
43 | | rdpContext* context; |
44 | | bool mainloop; |
45 | | } timer_entry_t; |
46 | | |
47 | | struct ALIGN64 freerdp_timer_s |
48 | | { |
49 | | rdpRdp* rdp; |
50 | | wArrayList* entries; |
51 | | HANDLE thread; |
52 | | HANDLE event; |
53 | | HANDLE mainevent; |
54 | | size_t maxIdx; |
55 | | bool running; |
56 | | }; |
57 | | |
58 | | FreeRDP_TimerID freerdp_timer_add(rdpContext* context, uint64_t intervalNS, |
59 | | FreeRDP_TimerCallback callback, void* userdata, bool mainloop) |
60 | 0 | { |
61 | 0 | WINPR_ASSERT(context); |
62 | 0 | WINPR_ASSERT(context->rdp); |
63 | | |
64 | | #if !defined(FREERDP_TIMER_SUPPORTED) |
65 | | WINPR_UNUSED(context); |
66 | | WINPR_UNUSED(intervalNS); |
67 | | WINPR_UNUSED(callback); |
68 | | WINPR_UNUSED(userdata); |
69 | | WINPR_UNUSED(mainloop); |
70 | | WLog_WARN(TAG, "Platform does not support freerdp_timer_* API"); |
71 | | return 0; |
72 | | #else |
73 | 0 | FreeRDPTimer* timer = context->rdp->timer; |
74 | 0 | WINPR_ASSERT(timer); |
75 | | |
76 | 0 | if ((intervalNS == 0) || !callback) |
77 | 0 | return false; |
78 | | |
79 | 0 | const uint64_t cur = winpr_GetTickCount64NS(); |
80 | 0 | const timer_entry_t entry = { .id = ++timer->maxIdx, |
81 | 0 | .intervallNS = intervalNS, |
82 | 0 | .nextRunTimeNS = cur + intervalNS, |
83 | 0 | .cb = callback, |
84 | 0 | .userdata = userdata, |
85 | 0 | .context = context, |
86 | 0 | .mainloop = mainloop }; |
87 | |
|
88 | 0 | if (!ArrayList_Append(timer->entries, &entry)) |
89 | 0 | return 0; |
90 | 0 | (void)SetEvent(timer->event); |
91 | 0 | return entry.id; |
92 | 0 | #endif |
93 | 0 | } |
94 | | |
95 | | static BOOL foreach_entry(void* data, WINPR_ATTR_UNUSED size_t index, va_list ap) |
96 | 0 | { |
97 | 0 | timer_entry_t* entry = data; |
98 | 0 | WINPR_ASSERT(entry); |
99 | | |
100 | 0 | FreeRDP_TimerID id = va_arg(ap, FreeRDP_TimerID); |
101 | |
|
102 | 0 | if (entry->id == id) |
103 | 0 | { |
104 | | /* Mark the timer to be disabled. |
105 | | * It will be removed on next rescheduling event |
106 | | */ |
107 | 0 | entry->intervallNS = 0; |
108 | 0 | return FALSE; |
109 | 0 | } |
110 | 0 | return TRUE; |
111 | 0 | } |
112 | | |
113 | | bool freerdp_timer_remove(rdpContext* context, FreeRDP_TimerID id) |
114 | 0 | { |
115 | 0 | WINPR_ASSERT(context); |
116 | 0 | WINPR_ASSERT(context->rdp); |
117 | | |
118 | 0 | FreeRDPTimer* timer = context->rdp->timer; |
119 | 0 | WINPR_ASSERT(timer); |
120 | | |
121 | 0 | return !ArrayList_ForEach(timer->entries, foreach_entry, id); |
122 | 0 | } |
123 | | |
124 | | static BOOL runTimerEvent(timer_entry_t* entry, uint64_t* now) |
125 | 0 | { |
126 | 0 | WINPR_ASSERT(entry); |
127 | | |
128 | 0 | entry->intervallNS = |
129 | 0 | entry->cb(entry->context, entry->userdata, entry->id, *now, entry->intervallNS); |
130 | 0 | *now = winpr_GetTickCount64NS(); |
131 | 0 | entry->nextRunTimeNS = *now + entry->intervallNS; |
132 | 0 | return TRUE; |
133 | 0 | } |
134 | | |
135 | | static BOOL runExpiredTimer(void* data, WINPR_ATTR_UNUSED size_t index, |
136 | | WINPR_ATTR_UNUSED va_list ap) |
137 | 0 | { |
138 | 0 | timer_entry_t* entry = data; |
139 | 0 | WINPR_ASSERT(entry); |
140 | 0 | WINPR_ASSERT(entry->cb); |
141 | | |
142 | | /* Skip all timers that have been deactivated. */ |
143 | 0 | if (entry->intervallNS == 0) |
144 | 0 | return TRUE; |
145 | | |
146 | 0 | uint64_t* now = va_arg(ap, uint64_t*); |
147 | 0 | WINPR_ASSERT(now); |
148 | | |
149 | 0 | bool* mainloop = va_arg(ap, bool*); |
150 | 0 | WINPR_ASSERT(mainloop); |
151 | | |
152 | 0 | if (entry->nextRunTimeNS > *now) |
153 | 0 | return TRUE; |
154 | | |
155 | 0 | if (entry->mainloop) |
156 | 0 | *mainloop = true; |
157 | 0 | else |
158 | 0 | runTimerEvent(entry, now); |
159 | |
|
160 | 0 | return TRUE; |
161 | 0 | } |
162 | | |
163 | | #if defined(FREERDP_TIMER_SUPPORTED) |
164 | | static uint64_t expire_and_reschedule(FreeRDPTimer* timer) |
165 | 0 | { |
166 | 0 | WINPR_ASSERT(timer); |
167 | | |
168 | 0 | bool mainloop = false; |
169 | 0 | uint64_t next = UINT64_MAX; |
170 | 0 | uint64_t now = winpr_GetTickCount64NS(); |
171 | |
|
172 | 0 | ArrayList_Lock(timer->entries); |
173 | 0 | ArrayList_ForEach(timer->entries, runExpiredTimer, &now, &mainloop); |
174 | 0 | if (mainloop) |
175 | 0 | (void)SetEvent(timer->mainevent); |
176 | |
|
177 | 0 | size_t pos = 0; |
178 | 0 | while (pos < ArrayList_Count(timer->entries)) |
179 | 0 | { |
180 | 0 | timer_entry_t* entry = ArrayList_GetItem(timer->entries, pos); |
181 | 0 | WINPR_ASSERT(entry); |
182 | 0 | if (entry->intervallNS == 0) |
183 | 0 | { |
184 | 0 | ArrayList_RemoveAt(timer->entries, pos); |
185 | 0 | continue; |
186 | 0 | } |
187 | 0 | if (next > entry->nextRunTimeNS) |
188 | 0 | next = entry->nextRunTimeNS; |
189 | 0 | pos++; |
190 | 0 | } |
191 | 0 | ArrayList_Unlock(timer->entries); |
192 | |
|
193 | 0 | if (next == UINT64_MAX) |
194 | 0 | return 0; |
195 | 0 | return next; |
196 | 0 | } |
197 | | |
198 | | static DWORD WINAPI timer_thread(LPVOID arg) |
199 | 0 | { |
200 | 0 | FreeRDPTimer* timer = arg; |
201 | 0 | WINPR_ASSERT(timer); |
202 | | |
203 | | // TODO: Currently we only support ms granularity, look for ways to improve |
204 | 0 | DWORD timeout = INFINITE; |
205 | 0 | HANDLE handles[2] = { utils_get_abort_event(timer->rdp), timer->event }; |
206 | |
|
207 | 0 | while (timer->running && |
208 | 0 | (WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, timeout) != WAIT_OBJECT_0)) |
209 | 0 | { |
210 | 0 | (void)ResetEvent(timer->event); |
211 | 0 | const uint64_t next = expire_and_reschedule(timer); |
212 | 0 | const uint64_t now = winpr_GetTickCount64NS(); |
213 | 0 | if (now >= next) |
214 | 0 | { |
215 | 0 | timeout = INFINITE; |
216 | 0 | continue; |
217 | 0 | } |
218 | | |
219 | 0 | const uint64_t diff = next - now; |
220 | 0 | const uint64_t diffMS = diff / 1000; |
221 | 0 | timeout = INFINITE; |
222 | 0 | if (diffMS < INFINITE) |
223 | 0 | timeout = (uint32_t)diffMS; |
224 | 0 | } |
225 | 0 | return 0; |
226 | 0 | } |
227 | | #endif |
228 | | |
229 | | void freerdp_timer_free(FreeRDPTimer* timer) |
230 | 0 | { |
231 | 0 | if (!timer) |
232 | 0 | return; |
233 | | |
234 | 0 | timer->running = false; |
235 | 0 | if (timer->event) |
236 | 0 | (void)SetEvent(timer->event); |
237 | |
|
238 | 0 | if (timer->thread) |
239 | 0 | { |
240 | 0 | (void)WaitForSingleObject(timer->thread, INFINITE); |
241 | 0 | CloseHandle(timer->thread); |
242 | 0 | } |
243 | 0 | if (timer->mainevent) |
244 | 0 | CloseHandle(timer->mainevent); |
245 | 0 | if (timer->event) |
246 | 0 | CloseHandle(timer->event); |
247 | 0 | ArrayList_Free(timer->entries); |
248 | 0 | free(timer); |
249 | 0 | } |
250 | | |
251 | | static void* entry_new(const void* val) |
252 | 0 | { |
253 | 0 | const timer_entry_t* entry = val; |
254 | 0 | if (!entry) |
255 | 0 | return NULL; |
256 | | |
257 | 0 | timer_entry_t* copy = calloc(1, sizeof(timer_entry_t)); |
258 | 0 | if (!copy) |
259 | 0 | return NULL; |
260 | 0 | *copy = *entry; |
261 | 0 | return copy; |
262 | 0 | } |
263 | | |
264 | | FreeRDPTimer* freerdp_timer_new(rdpRdp* rdp) |
265 | 0 | { |
266 | 0 | WINPR_ASSERT(rdp); |
267 | 0 | FreeRDPTimer* timer = calloc(1, sizeof(FreeRDPTimer)); |
268 | 0 | if (!timer) |
269 | 0 | return NULL; |
270 | 0 | timer->rdp = rdp; |
271 | |
|
272 | 0 | timer->entries = ArrayList_New(TRUE); |
273 | 0 | if (!timer->entries) |
274 | 0 | goto fail; |
275 | 0 | wObject* obj = ArrayList_Object(timer->entries); |
276 | 0 | WINPR_ASSERT(obj); |
277 | 0 | obj->fnObjectNew = entry_new; |
278 | 0 | obj->fnObjectFree = free; |
279 | |
|
280 | 0 | timer->event = CreateEventA(NULL, TRUE, FALSE, NULL); |
281 | 0 | if (!timer->event) |
282 | 0 | goto fail; |
283 | | |
284 | 0 | timer->mainevent = CreateEventA(NULL, TRUE, FALSE, NULL); |
285 | 0 | if (!timer->mainevent) |
286 | 0 | goto fail; |
287 | | |
288 | 0 | #if defined(FREERDP_TIMER_SUPPORTED) |
289 | 0 | timer->running = true; |
290 | 0 | timer->thread = CreateThread(NULL, 0, timer_thread, timer, 0, NULL); |
291 | 0 | if (!timer->thread) |
292 | 0 | goto fail; |
293 | 0 | #endif |
294 | 0 | return timer; |
295 | | |
296 | 0 | fail: |
297 | 0 | freerdp_timer_free(timer); |
298 | 0 | return NULL; |
299 | 0 | } |
300 | | |
301 | | static BOOL runExpiredTimerOnMainloop(void* data, WINPR_ATTR_UNUSED size_t index, |
302 | | WINPR_ATTR_UNUSED va_list ap) |
303 | 0 | { |
304 | 0 | timer_entry_t* entry = data; |
305 | 0 | WINPR_ASSERT(entry); |
306 | 0 | WINPR_ASSERT(entry->cb); |
307 | | |
308 | | /* Skip events not on mainloop */ |
309 | 0 | if (!entry->mainloop) |
310 | 0 | return TRUE; |
311 | | |
312 | | /* Skip all timers that have been deactivated. */ |
313 | 0 | if (entry->intervallNS == 0) |
314 | 0 | return TRUE; |
315 | | |
316 | 0 | uint64_t* now = va_arg(ap, uint64_t*); |
317 | 0 | WINPR_ASSERT(now); |
318 | | |
319 | 0 | if (entry->nextRunTimeNS > *now) |
320 | 0 | return TRUE; |
321 | | |
322 | 0 | runTimerEvent(entry, now); |
323 | 0 | return TRUE; |
324 | 0 | } |
325 | | |
326 | | bool freerdp_timer_poll(FreeRDPTimer* timer) |
327 | 0 | { |
328 | 0 | WINPR_ASSERT(timer); |
329 | | |
330 | 0 | if (WaitForSingleObject(timer->mainevent, 0) != WAIT_OBJECT_0) |
331 | 0 | return true; |
332 | | |
333 | 0 | ArrayList_Lock(timer->entries); |
334 | 0 | (void)ResetEvent(timer->mainevent); |
335 | 0 | uint64_t now = winpr_GetTickCount64NS(); |
336 | 0 | ArrayList_ForEach(timer->entries, runExpiredTimerOnMainloop, &now); |
337 | 0 | (void)SetEvent(timer->event); // Trigger a wakeup of timer thread to reschedule |
338 | 0 | ArrayList_Unlock(timer->entries); |
339 | 0 | return true; |
340 | 0 | } |
341 | | |
342 | | HANDLE freerdp_timer_get_event(FreeRDPTimer* timer) |
343 | 0 | { |
344 | 0 | WINPR_ASSERT(timer); |
345 | 0 | return timer->mainevent; |
346 | 0 | } |