diff --git a/include/freerdp/timer.h b/include/freerdp/timer.h new file mode 100644 index 000000000..63d0c058c --- /dev/null +++ b/include/freerdp/timer.h @@ -0,0 +1,101 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Timer implementation + * + * Copyright 2025 Armin Novak + * Copyright 2025 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** Type definition for timer IDs + * @since version 3.16.0 + */ + typedef uint64_t FreeRDP_TimerID; + + /** @brief Callback function pointer type definition. + * An expired timer will be called, depending on \ref mainloop argument of \ref + * freerdp_timer_add, background thread or mainloop. This also greatly influence jitter and + * precision of the call. If called by \b mainloop, which might be blocked, delays for up to + * 100ms are to be expected. If called from a background thread no locking is performed, so be + * sure to lock your resources where necessary. + * + * + * @param context The RDP context this timer belongs to + * @param userdata Custom userdata provided by \ref freerdp_timer_add + * @param timerID The timer ID that expired + * @param timestamp The current timestamp for the call. The base is not specified, but the + * resolution is in nanoseconds. + * @param interval The last interval value + * + * @return A new interval (might differ from the last one set) or \b 0 to disable the timer + * + * @since version 3.16.0 + */ + typedef uint64_t (*FreeRDP_TimerCallback)(rdpContext* context, void* userdata, + FreeRDP_TimerID timerID, uint64_t timestamp, + uint64_t interval); + + /** @brief Add a new timer to the list of running timers + * + * @note While the API allows nano second precision the execution time might vary depending on + * various circumstances. + * \b mainloop executed callbacks will have a huge jitter and execution times are expected to be + * delayed up to multiple 10s of milliseconds. Current implementation also does not guarantee + * more than 10ms granularity even for background thread callbacks, but that might improve with + * newer versions. + * + * @note Current implementation limits all timers to be executed by a single background thread. + * So ensure your callbacks are not blocking for a long time as both, \b mainloop and background + * thread executed callbacks will delay execution of other tasks. + * + * @param context The RDP context the timer belongs to + * @param intervalNS The (first) timer expiration interval in nanoseconds + * @param callback The function to be called when the timer expires. Must not be \b NULL + * @param userdata Custom userdata passed to the callback. The pointer is only passed, it is up + * to the user to ensure the data exists when the timer expires. + * @param mainloop \b true run the callback in mainloop context or \b false from background + * thread + * @return A new timer ID or \b 0 in case of failure + * @since version 3.16.0 + */ + FREERDP_API FreeRDP_TimerID freerdp_timer_add(rdpContext* context, uint64_t intervalNS, + FreeRDP_TimerCallback callback, void* userdata, + bool mainloop); + + /** @brief Remove a timer from the list of running timers + * + * @param context The RDP context the timer belongs to + * @param id The timer ID to remove + * + * @return \b true if the timer was removed, \b false otherwise + * @since version 3.16.0 + */ + FREERDP_API bool freerdp_timer_remove(rdpContext* context, FreeRDP_TimerID id); + +#ifdef __cplusplus +} +#endif diff --git a/libfreerdp/core/CMakeLists.txt b/libfreerdp/core/CMakeLists.txt index 0c1fc8f08..c31e7e5f5 100644 --- a/libfreerdp/core/CMakeLists.txt +++ b/libfreerdp/core/CMakeLists.txt @@ -149,6 +149,8 @@ set(${MODULE_PREFIX}_SRCS rdstls.h aad.c aad.h + timer.c + timer.h ) set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} ${${MODULE_PREFIX}_GATEWAY_SRCS}) diff --git a/libfreerdp/core/freerdp.c b/libfreerdp/core/freerdp.c index bee702dc3..1f1436ccd 100644 --- a/libfreerdp/core/freerdp.c +++ b/libfreerdp/core/freerdp.c @@ -390,16 +390,16 @@ DWORD freerdp_get_event_handles(rdpContext* context, HANDLE* events, DWORD count WINPR_ASSERT(context->rdp); WINPR_ASSERT(events || (count == 0)); - nCount += transport_get_event_handles(context->rdp->transport, events, count); - - if (nCount == 0) + const size_t rrc = rdp_get_event_handles(context->rdp, &events[nCount], count - nCount); + if (rrc == 0) return 0; + nCount += WINPR_ASSERTING_INT_CAST(uint32_t, rrc); + if (events && (nCount < count + 2)) { events[nCount++] = freerdp_channels_get_event_handle(context->instance); events[nCount++] = getChannelErrorEventHandle(context); - events[nCount++] = utils_get_abort_event(context->rdp); } else return 0; diff --git a/libfreerdp/core/rdp.c b/libfreerdp/core/rdp.c index ee2732949..6d3b1ead6 100644 --- a/libfreerdp/core/rdp.c +++ b/libfreerdp/core/rdp.c @@ -2279,6 +2279,8 @@ int rdp_check_fds(rdpRdp* rdp) if (status < 0) WLog_Print(rdp->log, WLOG_DEBUG, "transport_check_fds() - %i", status); + else + status = freerdp_timer_poll(rdp->timer); return status; } @@ -2301,6 +2303,46 @@ BOOL freerdp_get_stats(rdpRdp* rdp, UINT64* inBytes, UINT64* outBytes, UINT64* i return TRUE; } +static bool rdp_new_common(rdpRdp* rdp) +{ + WINPR_ASSERT(rdp); + + bool rc = false; + rdp->transport = transport_new(rdp->context); + if (!rdp->transport) + goto fail; + + if (rdp->io) + { + if (!transport_set_io_callbacks(rdp->transport, rdp->io)) + goto fail; + } + + rdp->aad = aad_new(rdp->context, rdp->transport); + if (!rdp->aad) + goto fail; + + rdp->nego = nego_new(rdp->transport); + if (!rdp->nego) + goto fail; + + rdp->mcs = mcs_new(rdp->transport); + if (!rdp->mcs) + goto fail; + + rdp->license = license_new(rdp); + if (!rdp->license) + goto fail; + + rdp->fastpath = fastpath_new(rdp); + if (!rdp->fastpath) + goto fail; + + rc = true; +fail: + return rc; +} + /** * Instantiate new RDP module. * @return new RDP module @@ -2308,9 +2350,8 @@ BOOL freerdp_get_stats(rdpRdp* rdp, UINT64* inBytes, UINT64* outBytes, UINT64* i rdpRdp* rdp_new(rdpContext* context) { - rdpRdp* rdp = NULL; DWORD flags = 0; - rdp = (rdpRdp*)calloc(1, sizeof(rdpRdp)); + rdpRdp* rdp = (rdpRdp*)calloc(1, sizeof(rdpRdp)); if (!rdp) return NULL; @@ -2356,9 +2397,7 @@ rdpRdp* rdp_new(rdpContext* context) #endif } - rdp->transport = transport_new(context); - - if (!rdp->transport) + if (!rdp_new_common(rdp)) goto fail; { @@ -2371,15 +2410,6 @@ rdpRdp* rdp_new(rdpContext* context) *rdp->io = *io; } - rdp->aad = aad_new(context, rdp->transport); - if (!rdp->aad) - goto fail; - - rdp->license = license_new(rdp); - - if (!rdp->license) - goto fail; - rdp->input = input_new(rdp); if (!rdp->input) @@ -2390,21 +2420,6 @@ rdpRdp* rdp_new(rdpContext* context) if (!rdp->update) goto fail; - rdp->fastpath = fastpath_new(rdp); - - if (!rdp->fastpath) - goto fail; - - rdp->nego = nego_new(rdp->transport); - - if (!rdp->nego) - goto fail; - - rdp->mcs = mcs_new(rdp->transport); - - if (!rdp->mcs) - goto fail; - rdp->redirection = redirection_new(); if (!rdp->redirection) @@ -2438,6 +2453,11 @@ rdpRdp* rdp_new(rdpContext* context) rdp->abortEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (!rdp->abortEvent) goto fail; + + rdp->timer = freerdp_timer_new(rdp); + if (!rdp->timer) + goto fail; + return rdp; fail: @@ -2462,12 +2482,14 @@ static void rdp_reset_free(rdpRdp* rdp) rdp->fips_decrypt = NULL; (void)security_unlock(rdp); + aad_free(rdp->aad); mcs_free(rdp->mcs); nego_free(rdp->nego); license_free(rdp->license); transport_free(rdp->transport); fastpath_free(rdp->fastpath); + rdp->aad = NULL; rdp->mcs = NULL; rdp->nego = NULL; rdp->license = NULL; @@ -2478,15 +2500,10 @@ static void rdp_reset_free(rdpRdp* rdp) BOOL rdp_reset(rdpRdp* rdp) { BOOL rc = TRUE; - rdpContext* context = NULL; - rdpSettings* settings = NULL; WINPR_ASSERT(rdp); - context = rdp->context; - WINPR_ASSERT(context); - - settings = rdp->settings; + rdpSettings* settings = rdp->settings; WINPR_ASSERT(settings); bulk_reset(rdp->bulk); @@ -2505,41 +2522,13 @@ BOOL rdp_reset(rdpRdp* rdp) if (!rc) goto fail; - rc = FALSE; - rdp->transport = transport_new(context); - if (!rdp->transport) - goto fail; - - if (rdp->io) - { - if (!transport_set_io_callbacks(rdp->transport, rdp->io)) - goto fail; - } - - aad_free(rdp->aad); - rdp->aad = aad_new(context, rdp->transport); - if (!rdp->aad) - goto fail; - - rdp->nego = nego_new(rdp->transport); - if (!rdp->nego) - goto fail; - - rdp->mcs = mcs_new(rdp->transport); - if (!rdp->mcs) + rc = rdp_new_common(rdp); + if (!rc) goto fail; if (!transport_set_layer(rdp->transport, TRANSPORT_LAYER_TCP)) goto fail; - rdp->license = license_new(rdp); - if (!rdp->license) - goto fail; - - rdp->fastpath = fastpath_new(rdp); - if (!rdp->fastpath) - goto fail; - rdp->errorInfo = 0; rc = rdp_finalize_reset_flags(rdp, TRUE); @@ -2556,6 +2545,7 @@ void rdp_free(rdpRdp* rdp) { if (rdp) { + freerdp_timer_free(rdp->timer); rdp_reset_free(rdp); freerdp_settings_free(rdp->settings); @@ -3147,3 +3137,18 @@ void rdp_log_build_warnings(rdpRdp* rdp) option_is_runtime_checks); log_build_warn_ssl(rdp); } + +size_t rdp_get_event_handles(rdpRdp* rdp, HANDLE* handles, uint32_t count) +{ + size_t nCount = transport_get_event_handles(rdp->transport, handles, count); + + if (nCount == 0) + return 0; + + if (count < nCount + 2UL) + return 0; + + handles[nCount++] = utils_get_abort_event(rdp); + handles[nCount++] = freerdp_timer_get_event(rdp->timer); + return nCount; +} diff --git a/libfreerdp/core/rdp.h b/libfreerdp/core/rdp.h index c0082251b..cfa8995d8 100644 --- a/libfreerdp/core/rdp.h +++ b/libfreerdp/core/rdp.h @@ -45,6 +45,7 @@ #include "redirection.h" #include "capabilities.h" #include "channels.h" +#include "timer.h" #include #include @@ -207,6 +208,7 @@ struct rdp_rdp wLog* log; char log_context[64]; WINPR_JSON* wellknown; + FreeRDPTimer* timer; }; FREERDP_LOCAL BOOL rdp_read_security_header(rdpRdp* rdp, wStream* s, UINT16* flags, UINT16* length); @@ -304,4 +306,6 @@ BOOL rdp_reset_runtime_settings(rdpRdp* rdp); void rdp_log_build_warnings(rdpRdp* rdp); +FREERDP_LOCAL size_t rdp_get_event_handles(rdpRdp* rdp, HANDLE* handles, uint32_t count); + #endif /* FREERDP_LIB_CORE_RDP_H */ diff --git a/libfreerdp/core/timer.c b/libfreerdp/core/timer.c new file mode 100644 index 000000000..74d63b475 --- /dev/null +++ b/libfreerdp/core/timer.c @@ -0,0 +1,321 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Timer implementation + * + * Copyright 2025 Armin Novak + * Copyright 2025 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include "rdp.h" +#include "utils.h" +#include "timer.h" + +typedef ALIGN64 struct +{ + FreeRDP_TimerID id; + uint64_t intervallNS; + uint64_t nextRunTimeNS; + FreeRDP_TimerCallback cb; + void* userdata; + rdpContext* context; + bool mainloop; +} timer_entry_t; + +struct ALIGN64 freerdp_timer_s +{ + rdpRdp* rdp; + wArrayList* entries; + HANDLE thread; + HANDLE event; + HANDLE mainevent; + size_t maxIdx; + bool running; +}; + +FreeRDP_TimerID freerdp_timer_add(rdpContext* context, uint64_t intervalNS, + FreeRDP_TimerCallback callback, void* userdata, bool mainloop) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(context->rdp); + + FreeRDPTimer* timer = context->rdp->timer; + WINPR_ASSERT(timer); + + if ((intervalNS == 0) || !callback) + return false; + + const uint64_t cur = winpr_GetTickCount64NS(); + const timer_entry_t entry = { .id = timer->maxIdx++, + .intervallNS = intervalNS, + .nextRunTimeNS = cur + intervalNS, + .cb = callback, + .userdata = userdata, + .context = context, + .mainloop = mainloop }; + + if (!ArrayList_Append(timer->entries, &entry)) + return 0; + (void)SetEvent(timer->event); + return entry.id; +} + +static BOOL foreach_entry(void* data, WINPR_ATTR_UNUSED size_t index, va_list ap) +{ + timer_entry_t* entry = data; + WINPR_ASSERT(entry); + + FreeRDP_TimerID id = va_arg(ap, FreeRDP_TimerID); + + if (entry->id == id) + { + /* Mark the timer to be disabled. + * It will be removed on next rescheduling event + */ + entry->intervallNS = 0; + return FALSE; + } + return TRUE; +} + +bool freerdp_timer_remove(rdpContext* context, FreeRDP_TimerID id) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(context->rdp); + + FreeRDPTimer* timer = context->rdp->timer; + WINPR_ASSERT(timer); + + return !ArrayList_ForEach(timer->entries, foreach_entry, id); +} + +static BOOL runTimerEvent(timer_entry_t* entry, uint64_t* now) +{ + WINPR_ASSERT(entry); + + entry->intervallNS = + entry->cb(entry->context, entry->userdata, entry->id, *now, entry->intervallNS); + *now = winpr_GetTickCount64NS(); + entry->nextRunTimeNS = *now + entry->intervallNS; + return TRUE; +} + +static BOOL runExpiredTimer(void* data, WINPR_ATTR_UNUSED size_t index, + WINPR_ATTR_UNUSED va_list ap) +{ + timer_entry_t* entry = data; + WINPR_ASSERT(entry); + WINPR_ASSERT(entry->cb); + + /* Skip all timers that have been deactivated. */ + if (entry->intervallNS == 0) + return TRUE; + + uint64_t* now = va_arg(ap, uint64_t*); + WINPR_ASSERT(now); + + bool* mainloop = va_arg(ap, bool*); + WINPR_ASSERT(mainloop); + + if (entry->nextRunTimeNS > *now) + return TRUE; + + if (entry->mainloop) + *mainloop = true; + else + runTimerEvent(entry, now); + + return TRUE; +} + +static uint64_t expire_and_reschedule(FreeRDPTimer* timer) +{ + WINPR_ASSERT(timer); + + bool mainloop = false; + uint64_t next = UINT64_MAX; + uint64_t now = winpr_GetTickCount64NS(); + + ArrayList_Lock(timer->entries); + ArrayList_ForEach(timer->entries, runExpiredTimer, &now, &mainloop); + if (mainloop) + (void)SetEvent(timer->mainevent); + + size_t pos = 0; + while (pos < ArrayList_Count(timer->entries)) + { + timer_entry_t* entry = ArrayList_GetItem(timer->entries, pos); + WINPR_ASSERT(entry); + if (entry->intervallNS == 0) + { + ArrayList_RemoveAt(timer->entries, pos); + continue; + } + if (next > entry->nextRunTimeNS) + next = entry->nextRunTimeNS; + pos++; + } + ArrayList_Unlock(timer->entries); + + if (next == UINT64_MAX) + return 0; + return next; +} + +static DWORD WINAPI timer_thread(LPVOID arg) +{ + FreeRDPTimer* timer = arg; + WINPR_ASSERT(timer); + + // TODO: Currently we only support ms granularity, look for ways to improve + DWORD timeout = INFINITE; + HANDLE handles[2] = { utils_get_abort_event(timer->rdp), timer->event }; + + while (WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, timeout) != WAIT_OBJECT_0) + { + (void)ResetEvent(timer->event); + const uint64_t next = expire_and_reschedule(timer); + const uint64_t now = winpr_GetTickCount64NS(); + if (now >= next) + { + timeout = INFINITE; + continue; + } + + const uint64_t diff = next - now; + const uint64_t diffMS = diff / 1000; + timeout = MIN(INFINITE, (uint32_t)diffMS); + } + return 0; +} + +void freerdp_timer_free(FreeRDPTimer* timer) +{ + if (!timer) + return; + + if (timer->event) + (void)SetEvent(timer->event); + timer->running = false; + if (timer->thread) + { + (void)WaitForSingleObject(timer->thread, INFINITE); + CloseHandle(timer->thread); + } + if (timer->mainevent) + CloseHandle(timer->mainevent); + if (timer->event) + CloseHandle(timer->event); + ArrayList_Free(timer->entries); + free(timer); +} + +static void* entry_new(const void* val) +{ + const timer_entry_t* entry = val; + if (!entry) + return NULL; + + timer_entry_t* copy = calloc(1, sizeof(timer_entry_t)); + if (!copy) + return NULL; + *copy = *entry; + return copy; +} + +FreeRDPTimer* freerdp_timer_new(rdpRdp* rdp) +{ + WINPR_ASSERT(rdp); + FreeRDPTimer* timer = calloc(1, sizeof(FreeRDPTimer)); + if (!timer) + return NULL; + timer->rdp = rdp; + + timer->entries = ArrayList_New(TRUE); + if (!timer->entries) + goto fail; + wObject* obj = ArrayList_Object(timer->entries); + WINPR_ASSERT(obj); + obj->fnObjectNew = entry_new; + obj->fnObjectFree = free; + + timer->event = CreateEventA(NULL, TRUE, FALSE, NULL); + if (!timer->event) + goto fail; + + timer->mainevent = CreateEventA(NULL, TRUE, FALSE, NULL); + if (!timer->mainevent) + goto fail; + + timer->running = true; + timer->thread = CreateThread(NULL, 0, timer_thread, timer, 0, NULL); + if (!timer->thread) + goto fail; + return timer; + +fail: + freerdp_timer_free(timer); + return NULL; +} + +static BOOL runExpiredTimerOnMainloop(void* data, WINPR_ATTR_UNUSED size_t index, + WINPR_ATTR_UNUSED va_list ap) +{ + timer_entry_t* entry = data; + WINPR_ASSERT(entry); + WINPR_ASSERT(entry->cb); + + /* Skip events not on mainloop */ + if (!entry->mainloop) + return TRUE; + + /* Skip all timers that have been deactivated. */ + if (entry->intervallNS == 0) + return TRUE; + + uint64_t* now = va_arg(ap, uint64_t*); + WINPR_ASSERT(now); + + if (entry->nextRunTimeNS > *now) + return TRUE; + + runTimerEvent(entry, now); + return TRUE; +} + +bool freerdp_timer_poll(FreeRDPTimer* timer) +{ + WINPR_ASSERT(timer); + + if (WaitForSingleObject(timer->mainevent, 0) != WAIT_OBJECT_0) + return true; + + ArrayList_Lock(timer->entries); + (void)ResetEvent(timer->mainevent); + uint64_t now = winpr_GetTickCount64NS(); + ArrayList_ForEach(timer->entries, runExpiredTimerOnMainloop, &now); + (void)SetEvent(timer->event); // Trigger a wakeup of timer thread to reschedule + ArrayList_Unlock(timer->entries); + return true; +} + +HANDLE freerdp_timer_get_event(FreeRDPTimer* timer) +{ + WINPR_ASSERT(timer); + return timer->mainevent; +} diff --git a/libfreerdp/core/timer.h b/libfreerdp/core/timer.h new file mode 100644 index 000000000..0234cd96d --- /dev/null +++ b/libfreerdp/core/timer.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Timer implementation + * + * Copyright 2025 Armin Novak + * Copyright 2025 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +typedef struct freerdp_timer_s FreeRDPTimer; + +FREERDP_LOCAL void freerdp_timer_free(FreeRDPTimer* timer); + +WINPR_ATTR_MALLOC(freerdp_timer_free, 1) +FREERDP_LOCAL FreeRDPTimer* freerdp_timer_new(rdpRdp* rdp); + +FREERDP_LOCAL bool freerdp_timer_poll(FreeRDPTimer* timer); +FREERDP_LOCAL HANDLE freerdp_timer_get_event(FreeRDPTimer* timer);