[core,timer] Add a timer implementation

Adds a timer implementation (bound to a RDP context) that is capable of
handling multiple timers simultaneously.
This commit is contained in:
Armin Novak 2025-05-14 11:05:54 +02:00 committed by akallabeth
parent 374707d4fa
commit 72a09b1675
No known key found for this signature in database
GPG Key ID: A49454A3FC909FD5
7 changed files with 536 additions and 69 deletions

101
include/freerdp/timer.h Normal file
View File

@ -0,0 +1,101 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Timer implementation
*
* Copyright 2025 Armin Novak <anovak@thincast.com>
* 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 <stdint.h>
#include <stdbool.h>
#include <freerdp/api.h>
#include <freerdp/types.h>
#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

View File

@ -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})

View File

@ -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;

View File

@ -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;
}

View File

@ -45,6 +45,7 @@
#include "redirection.h"
#include "capabilities.h"
#include "channels.h"
#include "timer.h"
#include <freerdp/freerdp.h>
#include <freerdp/settings.h>
@ -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 */

321
libfreerdp/core/timer.c Normal file
View File

@ -0,0 +1,321 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Timer implementation
*
* Copyright 2025 Armin Novak <anovak@thincast.com>
* 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 <winpr/thread.h>
#include <winpr/collections.h>
#include <freerdp/timer.h>
#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;
}

34
libfreerdp/core/timer.h Normal file
View File

@ -0,0 +1,34 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Timer implementation
*
* Copyright 2025 Armin Novak <anovak@thincast.com>
* 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 <freerdp/api.h>
#include <freerdp/types.h>
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);