diff --git a/win32/CMakeLists.txt b/win32/CMakeLists.txt index f1ebf615..38742859 100644 --- a/win32/CMakeLists.txt +++ b/win32/CMakeLists.txt @@ -40,6 +40,7 @@ set(pal_win32_h_files ${pal_common_h_files} inc/c_pal/execution_engine_win32.h inc/c_pal/job_object_helper.h + inc/c_pal/robust_semaphore.h ) set(pal_win32_c_files @@ -64,6 +65,7 @@ set(pal_win32_c_files src/${gballoc_ll_c} src/${gballoc_hl_c} src/job_object_helper.c + src/robust_semaphore.c ) FILE(GLOB pal_win32_md_files "devdoc/*.md") diff --git a/win32/devdoc/robust_semaphore_requirements.md b/win32/devdoc/robust_semaphore_requirements.md new file mode 100644 index 00000000..988ea886 --- /dev/null +++ b/win32/devdoc/robust_semaphore_requirements.md @@ -0,0 +1,224 @@ +# robust_semaphore requirements + +## Overview + +`robust_semaphore` lets at most `max_count` holders, across all the processes on a machine, hold a unit of a named semaphore at the same time. Unlike a Windows semaphore, a unit held by a process that dies is given back. + +A typical use is to limit how many instances of an expensive activity run at the same time on a machine, for example how many replicas are being built, across all the processes that run them. + +`robust_semaphore` is only implemented for Windows. + +## Design + +### Why mutexes and not a Windows semaphore + +The count of a Windows semaphore is not given back when a process that took it dies, so a crash would permanently lower the limit. A mutex is owned by the thread that acquired it: when that thread (or its process) ends without releasing it, the mutex is abandoned and the next waiter acquires it (`WAIT_ABANDONED`). + +So a `robust_semaphore` named `name` with `max_count` units is made of `max_count` named mutexes `_0` ... `_`. Holding a unit means owning one of these mutexes. + +### Parts + +- **The robust semaphore object** (`ROBUST_SEMAPHORE_HANDLE`). Each user (for example each process or each component that needs a unit) creates its own. It opens the `max_count` mutexes once, at create. It supports one acquisition at a time: a new acquisition can be started once the previous one ended. An acquisition that got a unit ends when it is released (or when `max_hold_ms` expires); an acquisition that did not get a unit ends just before its `on_acquire_complete` is called (so the callback can start a new one). The worker thread releases the mutex of a released acquisition before it takes the next one. +- **The worker thread** (`robust_semaphore_worker_thread`). Started at create and joined at destroy. Because a mutex belongs to the thread that acquired it, the worker thread is the only thread that waits on and releases the mutexes. It waits for a unit, calls `on_acquire_complete`, holds the unit until it is released, releases it, and then waits for the next acquisition. +- **The events**, each with one job: + - **work event** (auto reset): an acquisition was requested, or the object is being destroyed. Wakes the worker thread when it is idle. + - **cancel event** (manual reset): stop waiting for a unit. + - **release event** (manual reset): stop holding the unit. + +### States of an acquisition + +| State | Entered when | Worker thread | Leaves when | +|---|---|---|---| +| IDLE | create, or the previous acquisition ended | waits for the work event | `robust_semaphore_acquire_async` (to WAITING) or `robust_semaphore_destroy` (worker thread exits) | +| WAITING | the worker thread picks up the acquisition | waits for the cancel event or any of the mutexes, up to `timeout_ms` | a unit is acquired (to HOLDING, `on_acquire_complete` called with `ROBUST_SEMAPHORE_ACQUIRE_OK`), or cancel/release/destroy, timeout or error (to IDLE, `on_acquire_complete` called with the result) | +| HOLDING | a mutex was acquired | calls `on_acquire_complete`, then waits for the release event, up to what is left of `max_hold_ms` | release/destroy, or `max_hold_ms` expired (the mutex is released early): the worker thread releases the mutex (to IDLE) | + +`on_acquire_complete` is always called on the worker thread, never on the thread calling `robust_semaphore_acquire_async`. Because the worker thread may acquire a free unit right away, it can run before `robust_semaphore_acquire_async` returns, so the caller must have everything the callback uses in place before calling `robust_semaphore_acquire_async`. If `robust_semaphore_acquire_async` fails, the callback is not called. + +`robust_semaphore_cancel` and `robust_semaphore_release` cannot take back a unit that the worker thread already acquired: if they race with the acquisition of a unit, `on_acquire_complete` can still be called with `ROBUST_SEMAPHORE_ACQUIRE_OK`. After `robust_semaphore_release` the unit is given back as soon as `on_acquire_complete` returns; after `robust_semaphore_cancel` the unit is held until `robust_semaphore_release` is called. + +`max_hold_ms` is measured from the moment the unit is acquired, including the time spent in `on_acquire_complete`. The worker thread cannot release the mutex while it is running `on_acquire_complete`, so a callback that runs longer than `max_hold_ms` delays the release until it returns. + +`robust_semaphore_destroy` must not be called from `on_acquire_complete`, because it waits for the worker thread. If it is, it cannot wait for the worker thread: it logs an error and the worker thread frees the object when the callback returns. + +## Exposed API + +```c +#define ROBUST_SEMAPHORE_MAX_COUNT 63 + +#define ROBUST_SEMAPHORE_NO_TIMEOUT UINT32_MAX + +#define ROBUST_SEMAPHORE_UNIT_INDEX_NONE UINT32_MAX + +#define ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES \ + ROBUST_SEMAPHORE_ACQUIRE_OK, \ + ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT, \ + ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, \ + ROBUST_SEMAPHORE_ACQUIRE_ERROR + +MU_DEFINE_ENUM(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); + +typedef struct ROBUST_SEMAPHORE_TAG* ROBUST_SEMAPHORE_HANDLE; + +typedef void (*ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE)(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index); + +MOCKABLE_FUNCTION(, ROBUST_SEMAPHORE_HANDLE, robust_semaphore_create, const char*, name, uint32_t, max_count, const char*, security_descriptor); +MOCKABLE_FUNCTION(, void, robust_semaphore_destroy, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +MOCKABLE_FUNCTION(, int, robust_semaphore_acquire_async, ROBUST_SEMAPHORE_HANDLE, robust_semaphore, uint32_t, timeout_ms, uint32_t, max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE, on_acquire_complete, void*, on_acquire_complete_context); +MOCKABLE_FUNCTION(, void, robust_semaphore_cancel, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +MOCKABLE_FUNCTION(, void, robust_semaphore_release, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +``` + +### robust_semaphore_create + +```c +MOCKABLE_FUNCTION(, ROBUST_SEMAPHORE_HANDLE, robust_semaphore_create, const char*, name, uint32_t, max_count, const char*, security_descriptor); +``` + +`robust_semaphore_create` creates a robust semaphore object for the semaphore `name` with `max_count` units. `name` follows the rules of kernel object names (for example `Global\my_semaphore` to share it between sessions). All the users of the same semaphore have to use the same `max_count`. `security_descriptor` is an optional security descriptor string (SDDL) applied to the mutexes when they are created; if it is `NULL` the mutexes get the default security descriptor. + +**SRS_ROBUST_SEMAPHORE_01_001: [** If `name` is `NULL` then `robust_semaphore_create` shall fail and return `NULL`. **]** + +**SRS_ROBUST_SEMAPHORE_01_002: [** If `max_count` is 0 then `robust_semaphore_create` shall fail and return `NULL`. **]** + +**SRS_ROBUST_SEMAPHORE_01_003: [** If `max_count` is greater than `ROBUST_SEMAPHORE_MAX_COUNT` then `robust_semaphore_create` shall fail and return `NULL`. **]** + +**SRS_ROBUST_SEMAPHORE_01_004: [** `robust_semaphore_create` shall allocate memory for the robust semaphore object. **]** + +**SRS_ROBUST_SEMAPHORE_01_005: [** `robust_semaphore_create` shall initialize a lock by calling `srw_lock_ll_init`. **]** + +**SRS_ROBUST_SEMAPHORE_01_006: [** If `security_descriptor` is not `NULL`, `robust_semaphore_create` shall convert it by calling `ConvertStringSecurityDescriptorToSecurityDescriptorA`. **]** + +**SRS_ROBUST_SEMAPHORE_01_007: [** For each unit `i`, `robust_semaphore_create` shall build the mutex name `_` by calling `sprintf_char`. **]** + +**SRS_ROBUST_SEMAPHORE_01_008: [** For each unit `i`, `robust_semaphore_create` shall create or open the mutex by calling `CreateMutexExA` with the converted security descriptor, the mutex name and `SYNCHRONIZE | MUTEX_MODIFY_STATE` access, and free the mutex name. **]** + +**SRS_ROBUST_SEMAPHORE_01_009: [** `robust_semaphore_create` shall free the converted security descriptor by calling `LocalFree` and log any failure. **]** + +**SRS_ROBUST_SEMAPHORE_01_010: [** `robust_semaphore_create` shall create an auto reset work event, a manual reset cancel event and a manual reset release event by calling `CreateEventA`. **]** + +**SRS_ROBUST_SEMAPHORE_01_011: [** `robust_semaphore_create` shall start the worker thread by calling `ThreadAPI_Create` with `robust_semaphore_worker_thread`. **]** + +**SRS_ROBUST_SEMAPHORE_01_012: [** `robust_semaphore_create` shall succeed and return the robust semaphore object. **]** + +**SRS_ROBUST_SEMAPHORE_01_013: [** If there are any failures then `robust_semaphore_create` shall close the handles it created by calling `CloseHandle` (logging any failure), free the memory it allocated and fail and return `NULL`. **]** + +### robust_semaphore_destroy + +```c +MOCKABLE_FUNCTION(, void, robust_semaphore_destroy, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +``` + +`robust_semaphore_destroy` releases the unit (if any), stops the worker thread and frees the robust semaphore object. If an acquisition is still waiting, `on_acquire_complete` is called with `ROBUST_SEMAPHORE_ACQUIRE_CANCELLED` before `robust_semaphore_destroy` returns. + +**SRS_ROBUST_SEMAPHORE_01_014: [** If `robust_semaphore` is `NULL` then `robust_semaphore_destroy` shall return. **]** + +**SRS_ROBUST_SEMAPHORE_01_015: [** `robust_semaphore_destroy` shall acquire the lock in exclusive mode. **]** + +**SRS_ROBUST_SEMAPHORE_01_016: [** `robust_semaphore_destroy` shall mark the object as being destroyed, release the current acquisition as `robust_semaphore_release` does and wake the worker thread by calling `SetEvent` on the work event. **]** + +**SRS_ROBUST_SEMAPHORE_01_017: [** If `robust_semaphore_destroy` is called on the worker thread, it shall log an error, mark the worker thread as the one that frees the object, release the lock and return. **]** + +**SRS_ROBUST_SEMAPHORE_01_018: [** Otherwise `robust_semaphore_destroy` shall release the lock and wait for the worker thread to end by calling `ThreadAPI_Join`. **]** + +**SRS_ROBUST_SEMAPHORE_01_053: [** If `ThreadAPI_Join` fails, `robust_semaphore_destroy` shall log an error and return without freeing the object, because the worker thread may still use it. **]** + +**SRS_ROBUST_SEMAPHORE_01_019: [** `robust_semaphore_destroy` shall close the mutexes and the events by calling `CloseHandle` (logging any failure), deinitialize the lock and free the robust semaphore object. **]** + +### robust_semaphore_acquire_async + +```c +MOCKABLE_FUNCTION(, int, robust_semaphore_acquire_async, ROBUST_SEMAPHORE_HANDLE, robust_semaphore, uint32_t, timeout_ms, uint32_t, max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE, on_acquire_complete, void*, on_acquire_complete_context); +``` + +`robust_semaphore_acquire_async` starts waiting for a unit. `timeout_ms` is how long to wait for a unit (0 tries once, `ROBUST_SEMAPHORE_NO_TIMEOUT` waits until a unit is free). `max_hold_ms` is how long the unit may be held before the worker thread releases it early (`ROBUST_SEMAPHORE_NO_TIMEOUT` for no limit). + +If `robust_semaphore_acquire_async` succeeds, `on_acquire_complete` is called exactly once, on the worker thread, never on the thread calling `robust_semaphore_acquire_async`. Because the worker thread may acquire a free unit right away, it can run before `robust_semaphore_acquire_async` returns, so the caller must have everything the callback uses in place before calling it. If `robust_semaphore_acquire_async` fails, `on_acquire_complete` is not called. + +**SRS_ROBUST_SEMAPHORE_01_020: [** If `robust_semaphore` is `NULL` then `robust_semaphore_acquire_async` shall fail and return a non-zero value. **]** + +**SRS_ROBUST_SEMAPHORE_01_021: [** If `on_acquire_complete` is `NULL` then `robust_semaphore_acquire_async` shall fail and return a non-zero value. **]** + +**SRS_ROBUST_SEMAPHORE_01_022: [** `robust_semaphore_acquire_async` shall acquire the lock in exclusive mode. **]** + +**SRS_ROBUST_SEMAPHORE_01_023: [** If the object is being destroyed or its worker thread ended, `robust_semaphore_acquire_async` shall fail and return a non-zero value. **]** + +**SRS_ROBUST_SEMAPHORE_01_024: [** If there is a current acquisition that was not released or ended, `robust_semaphore_acquire_async` shall fail and return a non-zero value. **]** + +**SRS_ROBUST_SEMAPHORE_01_025: [** `robust_semaphore_acquire_async` shall store `timeout_ms`, `max_hold_ms`, `on_acquire_complete` and `on_acquire_complete_context` as the new current acquisition and wake the worker thread by calling `SetEvent` on the work event. **]** + +**SRS_ROBUST_SEMAPHORE_01_026: [** `robust_semaphore_acquire_async` shall release the lock and succeed and return 0. **]** + +**SRS_ROBUST_SEMAPHORE_01_027: [** If `SetEvent` fails, `robust_semaphore_acquire_async` shall forget the new acquisition, release the lock and fail and return a non-zero value. **]** + +### robust_semaphore_cancel + +```c +MOCKABLE_FUNCTION(, void, robust_semaphore_cancel, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +``` + +`robust_semaphore_cancel` stops waiting for a unit. It has no effect if the unit was already acquired (use `robust_semaphore_release` to give it back). + +**SRS_ROBUST_SEMAPHORE_01_028: [** If `robust_semaphore` is `NULL` then `robust_semaphore_cancel` shall return. **]** + +**SRS_ROBUST_SEMAPHORE_01_029: [** `robust_semaphore_cancel` shall acquire the lock in exclusive mode. **]** + +**SRS_ROBUST_SEMAPHORE_01_030: [** If the current acquisition was not released and did not get a unit, `robust_semaphore_cancel` shall mark it as cancelled and signal the cancel event by calling `SetEvent`, logging any failure. **]** + +**SRS_ROBUST_SEMAPHORE_01_031: [** `robust_semaphore_cancel` shall release the lock. **]** + +### robust_semaphore_release + +```c +MOCKABLE_FUNCTION(, void, robust_semaphore_release, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +``` + +`robust_semaphore_release` gives back the unit of the current acquisition, or stops waiting for one. An acquisition that holds a unit ends right away (the worker thread releases the mutex after `robust_semaphore_release` returns). An acquisition that is still waiting ends when its `on_acquire_complete` is called with `ROBUST_SEMAPHORE_ACQUIRE_CANCELLED`. Calling it multiple times has no further effect. + +**SRS_ROBUST_SEMAPHORE_01_032: [** If `robust_semaphore` is `NULL` then `robust_semaphore_release` shall return. **]** + +**SRS_ROBUST_SEMAPHORE_01_033: [** `robust_semaphore_release` shall acquire the lock in exclusive mode. **]** + +**SRS_ROBUST_SEMAPHORE_01_034: [** If there is a current acquisition that was not released, `robust_semaphore_release` shall mark it as released and signal the cancel event and the release event by calling `SetEvent`, logging any failure. **]** + +**SRS_ROBUST_SEMAPHORE_01_051: [** If the current acquisition holds a unit, `robust_semaphore_release` shall end it. **]** + +**SRS_ROBUST_SEMAPHORE_01_035: [** `robust_semaphore_release` shall release the lock. **]** + +### robust_semaphore_worker_thread + +```c +static int robust_semaphore_worker_thread(void* context); +``` + +**SRS_ROBUST_SEMAPHORE_01_036: [** `robust_semaphore_worker_thread` shall record the id of the worker thread by calling `GetCurrentThreadId`. **]** + +**SRS_ROBUST_SEMAPHORE_01_037: [** If there is a new acquisition, `robust_semaphore_worker_thread` shall take it and reset the cancel event and the release event by calling `ResetEvent`, logging any failure. **]** + +**SRS_ROBUST_SEMAPHORE_01_038: [** If the acquisition was already cancelled or released, `robust_semaphore_worker_thread` shall end it and call `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_CANCELLED` and `ROBUST_SEMAPHORE_UNIT_INDEX_NONE` without waiting for a unit. **]** + +**SRS_ROBUST_SEMAPHORE_01_039: [** Otherwise `robust_semaphore_worker_thread` shall wait for the cancel event or any of the mutexes by calling `WaitForMultipleObjects` with the cancel event followed by the `max_count` mutexes, `bWaitAll` `FALSE` and `timeout_ms` as timeout (`INFINITE` if `timeout_ms` is `ROBUST_SEMAPHORE_NO_TIMEOUT`). **]** + +**SRS_ROBUST_SEMAPHORE_01_040: [** If `WaitForMultipleObjects` returns `WAIT_OBJECT_0` then `robust_semaphore_worker_thread` shall end the acquisition and call `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_CANCELLED` and `ROBUST_SEMAPHORE_UNIT_INDEX_NONE`. **]** + +**SRS_ROBUST_SEMAPHORE_01_041: [** If `WaitForMultipleObjects` returns `WAIT_OBJECT_0 + 1 + i` or `WAIT_ABANDONED_0 + 1 + i` then `robust_semaphore_worker_thread` shall record the time the unit was acquired by calling `timer_global_get_elapsed_ms`, mark the acquisition as holding unit `i` and call `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_OK` and `i`. **]** + +**SRS_ROBUST_SEMAPHORE_01_042: [** If `WaitForMultipleObjects` returns `WAIT_TIMEOUT` then `robust_semaphore_worker_thread` shall end the acquisition and call `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT` and `ROBUST_SEMAPHORE_UNIT_INDEX_NONE`. **]** + +**SRS_ROBUST_SEMAPHORE_01_043: [** If `WaitForMultipleObjects` returns any other value then `robust_semaphore_worker_thread` shall log the error, end the acquisition and call `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_ERROR` and `ROBUST_SEMAPHORE_UNIT_INDEX_NONE`. **]** + +**SRS_ROBUST_SEMAPHORE_01_044: [** `robust_semaphore_worker_thread` shall end an acquisition that did not get a unit before calling its `on_acquire_complete`, so that `on_acquire_complete` can start a new acquisition. **]** + +**SRS_ROBUST_SEMAPHORE_01_045: [** If a unit was acquired, after `on_acquire_complete` returns, `robust_semaphore_worker_thread` shall wait for the release event by calling `WaitForSingleObject` with what is left of `max_hold_ms` since the unit was acquired (measured by calling `timer_global_get_elapsed_ms`, 0 if `max_hold_ms` already passed, `INFINITE` if `max_hold_ms` is `ROBUST_SEMAPHORE_NO_TIMEOUT`). **]** + +**SRS_ROBUST_SEMAPHORE_01_046: [** If waiting for the release event times out then `robust_semaphore_worker_thread` shall log an error and release the mutex early by calling `ReleaseMutex`. **]** + +**SRS_ROBUST_SEMAPHORE_01_047: [** If a unit was acquired, `robust_semaphore_worker_thread` shall release the mutex by calling `ReleaseMutex` (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. **]** + +**SRS_ROBUST_SEMAPHORE_01_048: [** If there is no new acquisition and the object is being destroyed, `robust_semaphore_worker_thread` shall return 0. **]** + +**SRS_ROBUST_SEMAPHORE_01_052: [** If there is no new acquisition and the object is not being destroyed, `robust_semaphore_worker_thread` shall wait for the work event by calling `WaitForSingleObject` with `INFINITE` timeout and then check again for a new acquisition. **]** + +**SRS_ROBUST_SEMAPHORE_01_050: [** If `WaitForSingleObject` on the work event fails, `robust_semaphore_worker_thread` shall log the error, mark the worker thread as ended, end a new acquisition it did not take yet and call its `on_acquire_complete` with `ROBUST_SEMAPHORE_ACQUIRE_ERROR` and `ROBUST_SEMAPHORE_UNIT_INDEX_NONE`, and return 0. **]** + +**SRS_ROBUST_SEMAPHORE_01_049: [** If `robust_semaphore_destroy` was called on the worker thread, before returning `robust_semaphore_worker_thread` shall close the mutexes, the events and its own thread handle by calling `CloseHandle` (logging any failure), deinitialize the lock and free the robust semaphore object. **]** diff --git a/win32/inc/c_pal/robust_semaphore.h b/win32/inc/c_pal/robust_semaphore.h new file mode 100644 index 00000000..e33d0dc0 --- /dev/null +++ b/win32/inc/c_pal/robust_semaphore.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef ROBUST_SEMAPHORE_H +#define ROBUST_SEMAPHORE_H + +#ifdef __cplusplus +#include +#else +#include +#endif + +#include "macro_utils/macro_utils.h" + +#include "umock_c/umock_c_prod.h" + +/* a robust semaphore with N units is made of N mutexes, waited on together with the cancel event (MAXIMUM_WAIT_OBJECTS is 64) */ +#define ROBUST_SEMAPHORE_MAX_COUNT 63 + +/* wait for a unit until one is free / hold a unit with no time limit */ +#define ROBUST_SEMAPHORE_NO_TIMEOUT UINT32_MAX + +#define ROBUST_SEMAPHORE_UNIT_INDEX_NONE UINT32_MAX + +#define ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES \ + ROBUST_SEMAPHORE_ACQUIRE_OK, \ + ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT, \ + ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, \ + ROBUST_SEMAPHORE_ACQUIRE_ERROR + +MU_DEFINE_ENUM(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); + +typedef struct ROBUST_SEMAPHORE_TAG* ROBUST_SEMAPHORE_HANDLE; + +typedef void (*ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE)(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index); + +#ifdef __cplusplus +extern "C" +{ +#endif + MOCKABLE_FUNCTION(, ROBUST_SEMAPHORE_HANDLE, robust_semaphore_create, const char*, name, uint32_t, max_count, const char*, security_descriptor); + MOCKABLE_FUNCTION(, void, robust_semaphore_destroy, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); + MOCKABLE_FUNCTION(, int, robust_semaphore_acquire_async, ROBUST_SEMAPHORE_HANDLE, robust_semaphore, uint32_t, timeout_ms, uint32_t, max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE, on_acquire_complete, void*, on_acquire_complete_context); + MOCKABLE_FUNCTION(, void, robust_semaphore_cancel, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); + MOCKABLE_FUNCTION(, void, robust_semaphore_release, ROBUST_SEMAPHORE_HANDLE, robust_semaphore); +#ifdef __cplusplus +} +#endif + +#endif // ROBUST_SEMAPHORE_H diff --git a/win32/src/robust_semaphore.c b/win32/src/robust_semaphore.c new file mode 100644 index 00000000..d926c5b4 --- /dev/null +++ b/win32/src/robust_semaphore.c @@ -0,0 +1,712 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include +#include +#include + +#include "windows.h" +#include "sddl.h" + +#include "macro_utils/macro_utils.h" + +#include "c_logging/logger.h" + +#include "c_pal/gballoc_hl.h" +#include "c_pal/gballoc_hl_redirect.h" +#include "c_pal/interlocked.h" +#include "c_pal/srw_lock_ll.h" +#include "c_pal/string_utils.h" +#include "c_pal/threadapi.h" +#include "c_pal/timer.h" + +#include "c_pal/robust_semaphore.h" + +MU_DEFINE_ENUM_STRINGS(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); + +#define ACQUISITION_STATE_VALUES \ + ACQUISITION_STATE_NONE, \ + ACQUISITION_STATE_REQUESTED, \ + ACQUISITION_STATE_WAITING, \ + ACQUISITION_STATE_HOLDING + +MU_DEFINE_ENUM(ACQUISITION_STATE, ACQUISITION_STATE_VALUES); +MU_DEFINE_ENUM_STRINGS(ACQUISITION_STATE, ACQUISITION_STATE_VALUES); + +typedef struct ROBUST_SEMAPHORE_TAG +{ + uint32_t max_count; + HANDLE mutexes[ROBUST_SEMAPHORE_MAX_COUNT]; + + HANDLE work_event; + HANDLE cancel_event; + HANDLE release_event; + + THREAD_HANDLE worker_thread; + volatile_atomic int32_t worker_thread_id; + + /* guards all the fields below */ + SRW_LOCK_LL lock; + + bool destroying; + bool worker_frees_object; + bool worker_ended; + + ACQUISITION_STATE acquisition_state; + bool acquisition_cancelled; + bool acquisition_released; + uint32_t timeout_ms; + uint32_t max_hold_ms; + ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete; + void* on_acquire_complete_context; +} ROBUST_SEMAPHORE; + +static void close_handle(HANDLE handle, const char* handle_name) +{ + if (!CloseHandle(handle)) + { + LogLastError("failure in CloseHandle(%s=%p)", handle_name, handle); + } + else + { + /* do nothing, closed */ + } +} + +static void set_event(HANDLE event, const char* event_name) +{ + if (!SetEvent(event)) + { + LogLastError("failure in SetEvent(%s=%p)", event_name, event); + } + else + { + /* do nothing, signaled */ + } +} + +static void reset_event(HANDLE event, const char* event_name) +{ + if (!ResetEvent(event)) + { + LogLastError("failure in ResetEvent(%s=%p)", event_name, event); + } + else + { + /* do nothing, reset */ + } +} + +static void robust_semaphore_free(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + for (uint32_t i = 0; i < robust_semaphore->max_count; i++) + { + close_handle(robust_semaphore->mutexes[i], "mutex"); + } + + close_handle(robust_semaphore->release_event, "release_event"); + close_handle(robust_semaphore->cancel_event, "cancel_event"); + close_handle(robust_semaphore->work_event, "work_event"); + + srw_lock_ll_deinit(&robust_semaphore->lock); + free(robust_semaphore); +} + +/* called with the lock held */ +static void internal_release(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + if ( + (robust_semaphore->acquisition_state == ACQUISITION_STATE_NONE) || + robust_semaphore->acquisition_released + ) + { + /* do nothing, there is no acquisition or it was already released */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_034: [ If there is a current acquisition that was not released, robust_semaphore_release shall mark it as released and signal the cancel event and the release event by calling SetEvent, logging any failure. ]*/ + robust_semaphore->acquisition_released = true; + set_event(robust_semaphore->cancel_event, "cancel_event"); + set_event(robust_semaphore->release_event, "release_event"); + + if (robust_semaphore->acquisition_state != ACQUISITION_STATE_HOLDING) + { + /* do nothing, a waiting acquisition ends when its on_acquire_complete is called with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_051: [ If the current acquisition holds a unit, robust_semaphore_release shall end it. ]*/ + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + } + } +} + +static void end_acquisition_without_unit(ROBUST_SEMAPHORE_HANDLE robust_semaphore, ROBUST_SEMAPHORE_ACQUIRE_RESULT acquire_result, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete, void* on_acquire_complete_context) +{ + /*Codes_SRS_ROBUST_SEMAPHORE_01_044: [ robust_semaphore_worker_thread shall end an acquisition that did not get a unit before calling its on_acquire_complete, so that on_acquire_complete can start a new acquisition. ]*/ + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + on_acquire_complete(on_acquire_complete_context, acquire_result, ROBUST_SEMAPHORE_UNIT_INDEX_NONE); +} + +static void hold_unit(ROBUST_SEMAPHORE_HANDLE robust_semaphore, uint32_t unit_index, uint32_t max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete, void* on_acquire_complete_context) +{ + /*Codes_SRS_ROBUST_SEMAPHORE_01_041: [ If WaitForMultipleObjects returns WAIT_OBJECT_0 + 1 + i or WAIT_ABANDONED_0 + 1 + i then robust_semaphore_worker_thread shall record the time the unit was acquired by calling timer_global_get_elapsed_ms, mark the acquisition as holding unit i and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_OK and i. ]*/ + double acquired_ms = timer_global_get_elapsed_ms(); + + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + robust_semaphore->acquisition_state = ACQUISITION_STATE_HOLDING; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + on_acquire_complete(on_acquire_complete_context, ROBUST_SEMAPHORE_ACQUIRE_OK, unit_index); + + /*Codes_SRS_ROBUST_SEMAPHORE_01_045: [ If a unit was acquired, after on_acquire_complete returns, robust_semaphore_worker_thread shall wait for the release event by calling WaitForSingleObject with what is left of max_hold_ms since the unit was acquired (measured by calling timer_global_get_elapsed_ms, 0 if max_hold_ms already passed, INFINITE if max_hold_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ + DWORD hold_timeout_ms; + if (max_hold_ms == ROBUST_SEMAPHORE_NO_TIMEOUT) + { + hold_timeout_ms = INFINITE; + } + else + { + double held_ms = timer_global_get_elapsed_ms() - acquired_ms; + hold_timeout_ms = (held_ms >= (double)max_hold_ms) ? 0 : (DWORD)((double)max_hold_ms - held_ms); + } + + DWORD hold_wait_result = WaitForSingleObject(robust_semaphore->release_event, hold_timeout_ms); + if (hold_wait_result == WAIT_TIMEOUT) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_046: [ If waiting for the release event times out then robust_semaphore_worker_thread shall log an error and release the mutex early by calling ReleaseMutex. ]*/ + LogError("Unit %" PRIu32 " was held for more than max_hold_ms=%" PRIu32 " ms (including the time spent in on_acquire_complete), releasing it early", unit_index, max_hold_ms); + } + else if (hold_wait_result != WAIT_OBJECT_0) + { + LogLastError("failure in WaitForSingleObject(release_event=%p, %lu) returned %lu, releasing unit %" PRIu32 "", robust_semaphore->release_event, hold_timeout_ms, hold_wait_result, unit_index); + } + else + { + /* do nothing, released */ + } + + /*Codes_SRS_ROBUST_SEMAPHORE_01_047: [ If a unit was acquired, robust_semaphore_worker_thread shall release the mutex by calling ReleaseMutex (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. ]*/ + if (!ReleaseMutex(robust_semaphore->mutexes[unit_index])) + { + LogLastError("failure in ReleaseMutex(mutex=%p) for unit %" PRIu32 "", robust_semaphore->mutexes[unit_index], unit_index); + } + else + { + /* do nothing, the unit is free */ + } + + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + if (robust_semaphore->acquisition_state != ACQUISITION_STATE_HOLDING) + { + /* do nothing, robust_semaphore_release already ended the acquisition */ + } + else + { + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + } + srw_lock_ll_release_exclusive(&robust_semaphore->lock); +} + +static void run_acquisition(ROBUST_SEMAPHORE_HANDLE robust_semaphore, uint32_t timeout_ms, uint32_t max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete, void* on_acquire_complete_context) +{ + HANDLE wait_handles[1 + ROBUST_SEMAPHORE_MAX_COUNT]; + wait_handles[0] = robust_semaphore->cancel_event; + for (uint32_t i = 0; i < robust_semaphore->max_count; i++) + { + wait_handles[1 + i] = robust_semaphore->mutexes[i]; + } + + /*Codes_SRS_ROBUST_SEMAPHORE_01_039: [ Otherwise robust_semaphore_worker_thread shall wait for the cancel event or any of the mutexes by calling WaitForMultipleObjects with the cancel event followed by the max_count mutexes, bWaitAll FALSE and timeout_ms as timeout (INFINITE if timeout_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ + DWORD wait_result = WaitForMultipleObjects(1 + robust_semaphore->max_count, wait_handles, FALSE, (timeout_ms == ROBUST_SEMAPHORE_NO_TIMEOUT) ? INFINITE : timeout_ms); + + if (wait_result == WAIT_OBJECT_0) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_040: [ If WaitForMultipleObjects returns WAIT_OBJECT_0 then robust_semaphore_worker_thread shall end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ + end_acquisition_without_unit(robust_semaphore, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, on_acquire_complete, on_acquire_complete_context); + } + else if ( + (wait_result >= WAIT_OBJECT_0 + 1) && + (wait_result < WAIT_OBJECT_0 + 1 + robust_semaphore->max_count) + ) + { + hold_unit(robust_semaphore, wait_result - (WAIT_OBJECT_0 + 1), max_hold_ms, on_acquire_complete, on_acquire_complete_context); + } + else if ( + (wait_result >= WAIT_ABANDONED_0 + 1) && + (wait_result < WAIT_ABANDONED_0 + 1 + robust_semaphore->max_count) + ) + { + uint32_t unit_index = wait_result - (WAIT_ABANDONED_0 + 1); + LogWarning("Unit %" PRIu32 " was abandoned by its previous owner (its thread or process ended without releasing it), acquiring it", unit_index); + hold_unit(robust_semaphore, unit_index, max_hold_ms, on_acquire_complete, on_acquire_complete_context); + } + else if (wait_result == WAIT_TIMEOUT) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_042: [ If WaitForMultipleObjects returns WAIT_TIMEOUT then robust_semaphore_worker_thread shall end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ + LogWarning("Could not acquire one of the %" PRIu32 " units within %" PRIu32 " ms", robust_semaphore->max_count, timeout_ms); + end_acquisition_without_unit(robust_semaphore, ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT, on_acquire_complete, on_acquire_complete_context); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_043: [ If WaitForMultipleObjects returns any other value then robust_semaphore_worker_thread shall log the error, end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_ERROR and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ + LogLastError("failure in WaitForMultipleObjects(%" PRIu32 ", %p, FALSE, %" PRIu32 ") returned %lu", 1 + robust_semaphore->max_count, wait_handles, timeout_ms, wait_result); + end_acquisition_without_unit(robust_semaphore, ROBUST_SEMAPHORE_ACQUIRE_ERROR, on_acquire_complete, on_acquire_complete_context); + } +} + +static int robust_semaphore_worker_thread(void* context) +{ + ROBUST_SEMAPHORE_HANDLE robust_semaphore = context; + + /*Codes_SRS_ROBUST_SEMAPHORE_01_036: [ robust_semaphore_worker_thread shall record the id of the worker thread by calling GetCurrentThreadId. ]*/ + (void)interlocked_exchange(&robust_semaphore->worker_thread_id, (int32_t)GetCurrentThreadId()); + + bool exit_thread = false; + while (!exit_thread) + { + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + if (robust_semaphore->acquisition_state == ACQUISITION_STATE_REQUESTED) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_037: [ If there is a new acquisition, robust_semaphore_worker_thread shall take it and reset the cancel event and the release event by calling ResetEvent, logging any failure. ]*/ + reset_event(robust_semaphore->cancel_event, "cancel_event"); + reset_event(robust_semaphore->release_event, "release_event"); + + uint32_t timeout_ms = robust_semaphore->timeout_ms; + uint32_t max_hold_ms = robust_semaphore->max_hold_ms; + ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete = robust_semaphore->on_acquire_complete; + void* on_acquire_complete_context = robust_semaphore->on_acquire_complete_context; + + if ( + robust_semaphore->acquisition_cancelled || + robust_semaphore->acquisition_released + ) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_038: [ If the acquisition was already cancelled or released, robust_semaphore_worker_thread shall end it and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED and ROBUST_SEMAPHORE_UNIT_INDEX_NONE without waiting for a unit. ]*/ + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + on_acquire_complete(on_acquire_complete_context, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, ROBUST_SEMAPHORE_UNIT_INDEX_NONE); + } + else + { + robust_semaphore->acquisition_state = ACQUISITION_STATE_WAITING; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + run_acquisition(robust_semaphore, timeout_ms, max_hold_ms, on_acquire_complete, on_acquire_complete_context); + } + } + else if (robust_semaphore->destroying) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_048: [ If there is no new acquisition and the object is being destroyed, robust_semaphore_worker_thread shall return 0. ]*/ + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + exit_thread = true; + } + else + { + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + /*Codes_SRS_ROBUST_SEMAPHORE_01_052: [ If there is no new acquisition and the object is not being destroyed, robust_semaphore_worker_thread shall wait for the work event by calling WaitForSingleObject with INFINITE timeout and then check again for a new acquisition. ]*/ + DWORD wait_result = WaitForSingleObject(robust_semaphore->work_event, INFINITE); + if (wait_result == WAIT_OBJECT_0) + { + /* do nothing, check again */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_050: [ If WaitForSingleObject on the work event fails, robust_semaphore_worker_thread shall log the error, mark the worker thread as ended, end a new acquisition it did not take yet and call its on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_ERROR and ROBUST_SEMAPHORE_UNIT_INDEX_NONE, and return 0. ]*/ + LogLastError("failure in WaitForSingleObject(work_event=%p, INFINITE) returned %lu, the worker thread ends", robust_semaphore->work_event, wait_result); + + ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete = NULL; + void* on_acquire_complete_context = NULL; + + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + robust_semaphore->worker_ended = true; + if (robust_semaphore->acquisition_state != ACQUISITION_STATE_REQUESTED) + { + /* do nothing, no acquisition is waiting to be taken */ + } + else + { + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + on_acquire_complete = robust_semaphore->on_acquire_complete; + on_acquire_complete_context = robust_semaphore->on_acquire_complete_context; + } + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + if (on_acquire_complete == NULL) + { + /* do nothing, no acquisition to complete */ + } + else + { + on_acquire_complete(on_acquire_complete_context, ROBUST_SEMAPHORE_ACQUIRE_ERROR, ROBUST_SEMAPHORE_UNIT_INDEX_NONE); + } + + exit_thread = true; + } + } + } + + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + bool worker_frees_object = robust_semaphore->worker_frees_object; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + if (!worker_frees_object) + { + /* do nothing, robust_semaphore_destroy waits for this thread and frees the object */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_049: [ If robust_semaphore_destroy was called on the worker thread, before returning robust_semaphore_worker_thread shall close the mutexes, the events and its own thread handle by calling CloseHandle (logging any failure), deinitialize the lock and free the robust semaphore object. ]*/ + /* nobody joins this thread, on Windows THREAD_HANDLE is the thread HANDLE */ + close_handle((HANDLE)robust_semaphore->worker_thread, "worker_thread"); + robust_semaphore_free(robust_semaphore); + } + + return 0; +} + +ROBUST_SEMAPHORE_HANDLE robust_semaphore_create(const char* name, uint32_t max_count, const char* security_descriptor) +{ + ROBUST_SEMAPHORE_HANDLE result; + + if ( + /*Codes_SRS_ROBUST_SEMAPHORE_01_001: [ If name is NULL then robust_semaphore_create shall fail and return NULL. ]*/ + (name == NULL) || + /*Codes_SRS_ROBUST_SEMAPHORE_01_002: [ If max_count is 0 then robust_semaphore_create shall fail and return NULL. ]*/ + (max_count == 0) || + /*Codes_SRS_ROBUST_SEMAPHORE_01_003: [ If max_count is greater than ROBUST_SEMAPHORE_MAX_COUNT then robust_semaphore_create shall fail and return NULL. ]*/ + (max_count > ROBUST_SEMAPHORE_MAX_COUNT) + ) + { + LogError("Invalid arguments: const char* name=%s, uint32_t max_count=%" PRIu32 ", const char* security_descriptor=%s", + MU_P_OR_NULL(name), max_count, MU_P_OR_NULL(security_descriptor)); + result = NULL; + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_004: [ robust_semaphore_create shall allocate memory for the robust semaphore object. ]*/ + result = malloc(sizeof(ROBUST_SEMAPHORE)); + if (result == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogError("failure in malloc(sizeof(ROBUST_SEMAPHORE)=%zu)", sizeof(ROBUST_SEMAPHORE)); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_005: [ robust_semaphore_create shall initialize a lock by calling srw_lock_ll_init. ]*/ + if (srw_lock_ll_init(&result->lock) != 0) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogError("failure in srw_lock_ll_init(&result->lock=%p)", &result->lock); + } + else + { + PSECURITY_DESCRIPTOR converted_security_descriptor = NULL; + + if ( + (security_descriptor != NULL) && + /*Codes_SRS_ROBUST_SEMAPHORE_01_006: [ If security_descriptor is not NULL, robust_semaphore_create shall convert it by calling ConvertStringSecurityDescriptorToSecurityDescriptorA. ]*/ + !ConvertStringSecurityDescriptorToSecurityDescriptorA(security_descriptor, SDDL_REVISION_1, &converted_security_descriptor, NULL) + ) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogLastError("failure in ConvertStringSecurityDescriptorToSecurityDescriptorA(%s)", security_descriptor); + } + else + { + SECURITY_ATTRIBUTES security_attributes; + security_attributes.nLength = sizeof(security_attributes); + security_attributes.lpSecurityDescriptor = converted_security_descriptor; + security_attributes.bInheritHandle = FALSE; + + uint32_t i; + for (i = 0; i < max_count; i++) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_007: [ For each unit i, robust_semaphore_create shall build the mutex name _ by calling sprintf_char. ]*/ + char* mutex_name = sprintf_char("%s_%" PRIu32 "", name, i); + if (mutex_name == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogError("failure in sprintf_char building the name of mutex %" PRIu32 " of %s", i, name); + break; + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_008: [ For each unit i, robust_semaphore_create shall create or open the mutex by calling CreateMutexExA with the converted security descriptor, the mutex name and SYNCHRONIZE | MUTEX_MODIFY_STATE access, and free the mutex name. ]*/ + result->mutexes[i] = CreateMutexExA(&security_attributes, mutex_name, 0, SYNCHRONIZE | MUTEX_MODIFY_STATE); + if (result->mutexes[i] == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogLastError("failure in CreateMutexExA(%s)", mutex_name); + free(mutex_name); + break; + } + else + { + free(mutex_name); + } + } + } + + if (converted_security_descriptor == NULL) + { + /* do nothing, the mutexes have the default security descriptor */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_009: [ robust_semaphore_create shall free the converted security descriptor by calling LocalFree and log any failure. ]*/ + if (LocalFree(converted_security_descriptor) != NULL) + { + LogLastError("failure in LocalFree(converted_security_descriptor=%p)", converted_security_descriptor); + } + else + { + /* do nothing, freed */ + } + } + + if (i < max_count) + { + /* do nothing, close the mutexes that were created below */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_010: [ robust_semaphore_create shall create an auto reset work event, a manual reset cancel event and a manual reset release event by calling CreateEventA. ]*/ + result->work_event = CreateEventA(NULL, FALSE, FALSE, NULL); + if (result->work_event == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogLastError("failure in CreateEventA(NULL, FALSE, FALSE, NULL) for the work event"); + } + else + { + result->cancel_event = CreateEventA(NULL, TRUE, FALSE, NULL); + if (result->cancel_event == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogLastError("failure in CreateEventA(NULL, TRUE, FALSE, NULL) for the cancel event"); + } + else + { + result->release_event = CreateEventA(NULL, TRUE, FALSE, NULL); + if (result->release_event == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogLastError("failure in CreateEventA(NULL, TRUE, FALSE, NULL) for the release event"); + } + else + { + result->max_count = max_count; + (void)interlocked_exchange(&result->worker_thread_id, 0); + result->destroying = false; + result->worker_frees_object = false; + result->worker_ended = false; + result->acquisition_state = ACQUISITION_STATE_NONE; + result->acquisition_cancelled = false; + result->acquisition_released = false; + + /*Codes_SRS_ROBUST_SEMAPHORE_01_011: [ robust_semaphore_create shall start the worker thread by calling ThreadAPI_Create with robust_semaphore_worker_thread. ]*/ + if (ThreadAPI_Create(&result->worker_thread, robust_semaphore_worker_thread, result) != THREADAPI_OK) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ + LogError("failure in ThreadAPI_Create(&result->worker_thread=%p, robust_semaphore_worker_thread, result=%p)", &result->worker_thread, result); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_012: [ robust_semaphore_create shall succeed and return the robust semaphore object. ]*/ + goto all_ok; + } + + close_handle(result->release_event, "release_event"); + } + + close_handle(result->cancel_event, "cancel_event"); + } + + close_handle(result->work_event, "work_event"); + } + } + + for (uint32_t j = 0; j < i; j++) + { + close_handle(result->mutexes[j], "mutex"); + } + } + + srw_lock_ll_deinit(&result->lock); + } + + free(result); + } + + result = NULL; + } + +all_ok: + return result; +} + +void robust_semaphore_destroy(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + if (robust_semaphore == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_014: [ If robust_semaphore is NULL then robust_semaphore_destroy shall return. ]*/ + LogError("Invalid arguments: ROBUST_SEMAPHORE_HANDLE robust_semaphore=%p", robust_semaphore); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_015: [ robust_semaphore_destroy shall acquire the lock in exclusive mode. ]*/ + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + + /*Codes_SRS_ROBUST_SEMAPHORE_01_016: [ robust_semaphore_destroy shall mark the object as being destroyed, release the current acquisition as robust_semaphore_release does and wake the worker thread by calling SetEvent on the work event. ]*/ + robust_semaphore->destroying = true; + internal_release(robust_semaphore); + set_event(robust_semaphore->work_event, "work_event"); + + if ((DWORD)interlocked_add(&robust_semaphore->worker_thread_id, 0) == GetCurrentThreadId()) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_017: [ If robust_semaphore_destroy is called on the worker thread, it shall log an error, mark the worker thread as the one that frees the object, release the lock and return. ]*/ + LogError("robust_semaphore_destroy(robust_semaphore=%p) was called from on_acquire_complete, the worker thread frees the object when on_acquire_complete returns", robust_semaphore); + robust_semaphore->worker_frees_object = true; + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_018: [ Otherwise robust_semaphore_destroy shall release the lock and wait for the worker thread to end by calling ThreadAPI_Join. ]*/ + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + + int thread_result; + if (ThreadAPI_Join(robust_semaphore->worker_thread, &thread_result) != THREADAPI_OK) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_053: [ If ThreadAPI_Join fails, robust_semaphore_destroy shall log an error and return without freeing the object, because the worker thread may still use it. ]*/ + LogError("failure in ThreadAPI_Join(robust_semaphore->worker_thread=%p), not freeing robust_semaphore=%p because its worker thread may still use it", robust_semaphore->worker_thread, robust_semaphore); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_019: [ robust_semaphore_destroy shall close the mutexes and the events by calling CloseHandle (logging any failure), deinitialize the lock and free the robust semaphore object. ]*/ + robust_semaphore_free(robust_semaphore); + } + } + } +} + +int robust_semaphore_acquire_async(ROBUST_SEMAPHORE_HANDLE robust_semaphore, uint32_t timeout_ms, uint32_t max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete, void* on_acquire_complete_context) +{ + int result; + + if ( + /*Codes_SRS_ROBUST_SEMAPHORE_01_020: [ If robust_semaphore is NULL then robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ + (robust_semaphore == NULL) || + /*Codes_SRS_ROBUST_SEMAPHORE_01_021: [ If on_acquire_complete is NULL then robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ + (on_acquire_complete == NULL) + ) + { + LogError("Invalid arguments: ROBUST_SEMAPHORE_HANDLE robust_semaphore=%p, uint32_t timeout_ms=%" PRIu32 ", uint32_t max_hold_ms=%" PRIu32 ", ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete=%p, void* on_acquire_complete_context=%p", + robust_semaphore, timeout_ms, max_hold_ms, on_acquire_complete, on_acquire_complete_context); + result = MU_FAILURE; + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_022: [ robust_semaphore_acquire_async shall acquire the lock in exclusive mode. ]*/ + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + + if ( + robust_semaphore->destroying || + robust_semaphore->worker_ended + ) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_023: [ If the object is being destroyed or its worker thread ended, robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ + LogError("Cannot acquire robust_semaphore=%p, destroying=%d, worker_ended=%d", robust_semaphore, robust_semaphore->destroying, robust_semaphore->worker_ended); + result = MU_FAILURE; + } + else if (robust_semaphore->acquisition_state != ACQUISITION_STATE_NONE) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_024: [ If there is a current acquisition that was not released or ended, robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ + LogError("Cannot acquire robust_semaphore=%p, the current acquisition (state %" PRI_MU_ENUM ") did not end", robust_semaphore, MU_ENUM_VALUE(ACQUISITION_STATE, robust_semaphore->acquisition_state)); + result = MU_FAILURE; + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_025: [ robust_semaphore_acquire_async shall store timeout_ms, max_hold_ms, on_acquire_complete and on_acquire_complete_context as the new current acquisition and wake the worker thread by calling SetEvent on the work event. ]*/ + robust_semaphore->timeout_ms = timeout_ms; + robust_semaphore->max_hold_ms = max_hold_ms; + robust_semaphore->on_acquire_complete = on_acquire_complete; + robust_semaphore->on_acquire_complete_context = on_acquire_complete_context; + robust_semaphore->acquisition_cancelled = false; + robust_semaphore->acquisition_released = false; + robust_semaphore->acquisition_state = ACQUISITION_STATE_REQUESTED; + + if (!SetEvent(robust_semaphore->work_event)) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_027: [ If SetEvent fails, robust_semaphore_acquire_async shall forget the new acquisition, release the lock and fail and return a non-zero value. ]*/ + LogLastError("failure in SetEvent(work_event=%p)", robust_semaphore->work_event); + robust_semaphore->acquisition_state = ACQUISITION_STATE_NONE; + result = MU_FAILURE; + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_026: [ robust_semaphore_acquire_async shall release the lock and succeed and return 0. ]*/ + result = 0; + } + } + + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + } + + return result; +} + +void robust_semaphore_cancel(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + if (robust_semaphore == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_028: [ If robust_semaphore is NULL then robust_semaphore_cancel shall return. ]*/ + LogError("Invalid arguments: ROBUST_SEMAPHORE_HANDLE robust_semaphore=%p", robust_semaphore); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_029: [ robust_semaphore_cancel shall acquire the lock in exclusive mode. ]*/ + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + + if ( + (robust_semaphore->acquisition_state != ACQUISITION_STATE_REQUESTED) && + (robust_semaphore->acquisition_state != ACQUISITION_STATE_WAITING) + ) + { + /* do nothing, no acquisition is waiting for a unit */ + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_030: [ If the current acquisition was not released and did not get a unit, robust_semaphore_cancel shall mark it as cancelled and signal the cancel event by calling SetEvent, logging any failure. ]*/ + robust_semaphore->acquisition_cancelled = true; + set_event(robust_semaphore->cancel_event, "cancel_event"); + } + + /*Codes_SRS_ROBUST_SEMAPHORE_01_031: [ robust_semaphore_cancel shall release the lock. ]*/ + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + } +} + +void robust_semaphore_release(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + if (robust_semaphore == NULL) + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_032: [ If robust_semaphore is NULL then robust_semaphore_release shall return. ]*/ + LogError("Invalid arguments: ROBUST_SEMAPHORE_HANDLE robust_semaphore=%p", robust_semaphore); + } + else + { + /*Codes_SRS_ROBUST_SEMAPHORE_01_033: [ robust_semaphore_release shall acquire the lock in exclusive mode. ]*/ + srw_lock_ll_acquire_exclusive(&robust_semaphore->lock); + + internal_release(robust_semaphore); + + /*Codes_SRS_ROBUST_SEMAPHORE_01_035: [ robust_semaphore_release shall release the lock. ]*/ + srw_lock_ll_release_exclusive(&robust_semaphore->lock); + } +} diff --git a/win32/tests/CMakeLists.txt b/win32/tests/CMakeLists.txt index af07be5f..4ef138b5 100644 --- a/win32/tests/CMakeLists.txt +++ b/win32/tests/CMakeLists.txt @@ -35,15 +35,18 @@ if(${run_unittests}) build_test_folder(gballoc_hl_passthrough_ut) build_test_folder(job_object_helper_ut) + build_test_folder(robust_semaphore_ut) endif() if(${run_int_tests}) add_subdirectory(job_object_helper_tester) add_subdirectory(process_watchdog_int_child) + add_subdirectory(robust_semaphore_tester) build_test_folder(malloc_multi_flex_int) build_test_folder(string_utils_int) build_test_folder(job_object_helper_int) build_test_folder(process_watchdog_int) + build_test_folder(robust_semaphore_int) if((${GBALLOC_LL_TYPE} STREQUAL "MIMALLOC")) build_test_folder(gballoc_ll_mimalloc_int) diff --git a/win32/tests/robust_semaphore_int/CMakeLists.txt b/win32/tests/robust_semaphore_int/CMakeLists.txt new file mode 100644 index 00000000..943c0510 --- /dev/null +++ b/win32/tests/robust_semaphore_int/CMakeLists.txt @@ -0,0 +1,24 @@ +#Copyright (c) Microsoft. All rights reserved. +#Licensed under the MIT license. See LICENSE file in the project root for full license information. + +set(theseTestsName robust_semaphore_int) + +set(${theseTestsName}_test_files + ${theseTestsName}.c +) + +set(${theseTestsName}_c_files +) + +set(${theseTestsName}_h_files +) + +build_test_artifacts(${theseTestsName} "tests/c_pal/win32" ADDITIONAL_LIBS c_pal) + +# the tests start robust_semaphore_tester to hold a unit from another process +if(TARGET ${theseTestsName}_exe_${CMAKE_PROJECT_NAME}) + add_dependencies(${theseTestsName}_exe_${CMAKE_PROJECT_NAME} robust_semaphore_tester) +endif() +if(TARGET ${theseTestsName}_dll_${CMAKE_PROJECT_NAME}) + add_dependencies(${theseTestsName}_dll_${CMAKE_PROJECT_NAME} robust_semaphore_tester) +endif() diff --git a/win32/tests/robust_semaphore_int/robust_semaphore_int.c b/win32/tests/robust_semaphore_int/robust_semaphore_int.c new file mode 100644 index 00000000..b40bd994 --- /dev/null +++ b/win32/tests/robust_semaphore_int/robust_semaphore_int.c @@ -0,0 +1,810 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include +#include +#include +#include +#include +#include + +#include "windows.h" + +#include "testrunnerswitcher.h" + +#include "macro_utils/macro_utils.h" + +#include "c_logging/logger.h" + +#include "c_pal/gballoc_hl.h" +#include "c_pal/interlocked.h" +#include "c_pal/sync.h" +#include "c_pal/threadapi.h" +#include "c_pal/timer.h" + +#include "c_pal/robust_semaphore.h" + +TEST_DEFINE_ENUM_TYPE(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); + +#define TEST_WAIT_TIMEOUT_MS 10000 +#define TEST_NO_MORE_ACQUISITIONS_WAIT_MS 300 +#define TEST_TESTER_PROCESS_TIMEOUT_MS 60000 +#define TEST_UNIT_PROBE_INTERVAL_MS 20 +#define MAX_TEST_USERS 8 + +/* a user of the robust semaphore, for example one of the processes or components that need a unit */ +typedef struct TEST_USER_TAG +{ + ROBUST_SEMAPHORE_HANDLE robust_semaphore; + volatile_atomic int32_t completed; /* 1 once on_acquire_complete was called */ + volatile_atomic int32_t result; /* ROBUST_SEMAPHORE_ACQUIRE_RESULT */ + volatile_atomic int32_t unit_index; + volatile_atomic int32_t released; + volatile_atomic int64_t completed_ms; + volatile_atomic int32_t slow_callback_returned_at_completion; +} TEST_USER; + +static TEST_USER test_users[MAX_TEST_USERS]; +static volatile_atomic int32_t completed_count; +static volatile_atomic int32_t current_holders; +static volatile_atomic int32_t max_holders; +static volatile_atomic int32_t slow_callback_returned; +static char test_semaphore_name[96]; + +/* the tester process started by a test, killed and closed by method_cleanup (also when the test fails) */ +static HANDLE test_job; +static PROCESS_INFORMATION test_tester_process; +static HANDLE test_held_event; + +static void on_acquire_complete(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index) +{ + TEST_USER* test_user = context; + + if (result != ROBUST_SEMAPHORE_ACQUIRE_OK) + { + /* do nothing, no unit is held */ + } + else + { + int32_t holders = interlocked_increment(¤t_holders); + int32_t current_max; + while ((current_max = interlocked_add(&max_holders, 0)) < holders) + { + (void)interlocked_compare_exchange(&max_holders, holders, current_max); + } + } + + (void)interlocked_exchange(&test_user->unit_index, (int32_t)unit_index); + (void)interlocked_exchange(&test_user->result, result); + (void)interlocked_exchange_64(&test_user->completed_ms, (int64_t)timer_global_get_elapsed_ms()); + (void)interlocked_exchange(&test_user->slow_callback_returned_at_completion, interlocked_add(&slow_callback_returned, 0)); + (void)interlocked_exchange(&test_user->completed, 1); + (void)interlocked_increment(&completed_count); + wake_by_address_all(&completed_count); +} + +static void create_test_user(TEST_USER* test_user, const char* name, uint32_t max_count, const char* security_descriptor) +{ + test_user->robust_semaphore = robust_semaphore_create(name, max_count, security_descriptor); + ASSERT_IS_NOT_NULL(test_user->robust_semaphore); + (void)interlocked_exchange(&test_user->completed, 0); + (void)interlocked_exchange(&test_user->result, ROBUST_SEMAPHORE_ACQUIRE_ERROR); + (void)interlocked_exchange(&test_user->unit_index, (int32_t)ROBUST_SEMAPHORE_UNIT_INDEX_NONE); + (void)interlocked_exchange(&test_user->released, 0); +} + +static void create_test_users(uint32_t count, const char* name, uint32_t max_count) +{ + for (uint32_t i = 0; i < count; i++) + { + create_test_user(&test_users[i], name, max_count, NULL); + } +} + +static void acquire_test_user_with_callback(TEST_USER* test_user, uint32_t timeout_ms, uint32_t max_hold_ms, ROBUST_SEMAPHORE_ON_ACQUIRE_COMPLETE on_acquire_complete_callback) +{ + (void)interlocked_exchange(&test_user->completed, 0); + (void)interlocked_exchange(&test_user->released, 0); + ASSERT_ARE_EQUAL(int, 0, robust_semaphore_acquire_async(test_user->robust_semaphore, timeout_ms, max_hold_ms, on_acquire_complete_callback, test_user)); +} + +static void acquire_test_user(TEST_USER* test_user, uint32_t timeout_ms, uint32_t max_hold_ms) +{ + acquire_test_user_with_callback(test_user, timeout_ms, max_hold_ms, on_acquire_complete); +} + +/* the acquisition did not complete yet, it is waiting for a unit */ +static void assert_test_user_waits(TEST_USER* test_user) +{ + Sleep(TEST_NO_MORE_ACQUISITIONS_WAIT_MS); + ASSERT_ARE_EQUAL(int32_t, 0, interlocked_add(&test_user->completed, 0)); +} + +static bool test_user_holds_a_unit(TEST_USER* test_user) +{ + return + (interlocked_add(&test_user->completed, 0) == 1) && + (interlocked_add(&test_user->result, 0) == ROBUST_SEMAPHORE_ACQUIRE_OK) && + (interlocked_add(&test_user->released, 0) == 0); +} + +static void assert_holders_have_different_units(uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) + { + for (uint32_t j = i + 1; j < count; j++) + { + if (test_user_holds_a_unit(&test_users[i]) && test_user_holds_a_unit(&test_users[j])) + { + ASSERT_ARE_NOT_EQUAL(int32_t, interlocked_add(&test_users[i].unit_index, 0), interlocked_add(&test_users[j].unit_index, 0)); + } + else + { + /* do nothing, at least one of them does not hold a unit */ + } + } + } +} + +static void release_test_user(TEST_USER* test_user) +{ + if ( + (interlocked_add(&test_user->completed, 0) == 1) && + (interlocked_add(&test_user->result, 0) == ROBUST_SEMAPHORE_ACQUIRE_OK) && + (interlocked_exchange(&test_user->released, 1) == 0) + ) + { + (void)interlocked_decrement(¤t_holders); + } + else + { + /* do nothing, the user does not hold a unit */ + } + robust_semaphore_release(test_user->robust_semaphore); +} + +static void wait_for_completed_count(int32_t expected_count) +{ + double start = timer_global_get_elapsed_ms(); + int32_t current; + while ((current = interlocked_add(&completed_count, 0)) < expected_count) + { + ASSERT_IS_TRUE(timer_global_get_elapsed_ms() - start < TEST_WAIT_TIMEOUT_MS, "timed out waiting for %" PRId32 " completions, got %" PRId32 "", expected_count, current); + (void)wait_on_address(&completed_count, current, 100); + } +} + +static ROBUST_SEMAPHORE_ACQUIRE_RESULT get_result(TEST_USER* test_user) +{ + return (ROBUST_SEMAPHORE_ACQUIRE_RESULT)interlocked_add(&test_user->result, 0); +} + +/* robust_semaphore_destroy completes a pending acquisition and waits for the worker thread */ +static void destroy_test_users(void) +{ + for (uint32_t i = 0; i < MAX_TEST_USERS; i++) + { + if (test_users[i].robust_semaphore == NULL) + { + /* do nothing, not created or already destroyed */ + } + else + { + robust_semaphore_destroy(test_users[i].robust_semaphore); + test_users[i].robust_semaphore = NULL; + } + } +} + +/* looks at the mutexes from outside of the module: a unit is held if another thread owns its mutex */ +static uint32_t get_held_unit_count(const char* name, uint32_t max_count) +{ + uint32_t held_count = 0; + + for (uint32_t i = 0; i < max_count; i++) + { + char mutex_name[128]; + ASSERT_IS_TRUE(snprintf(mutex_name, sizeof(mutex_name), "%s_%" PRIu32 "", name, i) > 0); + + HANDLE mutex = OpenMutexA(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, mutex_name); + DWORD open_error = GetLastError(); + if (mutex == NULL) + { + /* nobody has the mutex open, so nobody holds it */ + ASSERT_ARE_EQUAL(uint32_t, ERROR_FILE_NOT_FOUND, (uint32_t)open_error, "OpenMutexA(%s) failed", mutex_name); + } + else + { + DWORD wait_result = WaitForSingleObject(mutex, 0); + if ( + (wait_result == WAIT_OBJECT_0) || + (wait_result == WAIT_ABANDONED) + ) + { + ASSERT_IS_TRUE(ReleaseMutex(mutex)); + } + else + { + ASSERT_ARE_EQUAL(uint32_t, WAIT_TIMEOUT, (uint32_t)wait_result, "WaitForSingleObject(%s) failed", mutex_name); + held_count++; + } + (void)CloseHandle(mutex); + } + } + + return held_count; +} + +/* the worker threads release the mutexes asynchronously, so wait a bit for the expected state */ +static void wait_for_held_unit_count(const char* name, uint32_t max_count, uint32_t expected_held_count) +{ + double start = timer_global_get_elapsed_ms(); + uint32_t held_count; + while ((held_count = get_held_unit_count(name, max_count)) != expected_held_count) + { + ASSERT_IS_TRUE(timer_global_get_elapsed_ms() - start < TEST_WAIT_TIMEOUT_MS, "%" PRIu32 " units of %s are held, expected %" PRIu32 "", held_count, name, expected_held_count); + Sleep(TEST_UNIT_PROBE_INTERVAL_MS); + } +} + +/* the tester is built in its own folder next to the folder of the test exe (and of the test dll, which vstest loads in its own host process) */ +static void get_tester_path(char* tester_path, DWORD tester_path_size) +{ + HMODULE test_module; + ASSERT_IS_TRUE(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)&test_users, &test_module)); + DWORD length = GetModuleFileNameA(test_module, tester_path, tester_path_size); + ASSERT_IS_TRUE((length > 0) && (length < tester_path_size)); + + char* last_separator = strrchr(tester_path, '\\'); + ASSERT_IS_NOT_NULL(last_separator); + last_separator[1] = '\0'; + ASSERT_ARE_EQUAL(int, 0, strcat_s(tester_path, tester_path_size, "..\\robust_semaphore_tester\\robust_semaphore_tester.exe")); +} + +/* starts a process that holds a unit of the semaphore, in a job that kills it when the test ends */ +static void start_tester_process(const char* name) +{ + char held_event_name[128]; + ASSERT_IS_TRUE(snprintf(held_event_name, sizeof(held_event_name), "%s_held", name) > 0); + test_held_event = CreateEventA(NULL, TRUE, FALSE, held_event_name); + ASSERT_IS_NOT_NULL(test_held_event); + + test_job = CreateJobObjectA(NULL, NULL); + ASSERT_IS_NOT_NULL(test_job); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits = { 0 }; + job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + ASSERT_IS_TRUE(SetInformationJobObject(test_job, JobObjectExtendedLimitInformation, &job_limits, sizeof(job_limits))); + + char tester_path[MAX_PATH]; + get_tester_path(tester_path, MAX_PATH); + char command_line[MAX_PATH + 256]; + ASSERT_IS_TRUE(snprintf(command_line, sizeof(command_line), "\"%s\" %s %s", tester_path, name, held_event_name) > 0); + + STARTUPINFOA startup_info = { 0 }; + startup_info.cb = sizeof(startup_info); + BOOL created = CreateProcessA(tester_path, command_line, NULL, NULL, FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, NULL, NULL, &startup_info, &test_tester_process); + ASSERT_IS_TRUE(created, "CreateProcessA(%s) failed with %lu", tester_path, GetLastError()); + ASSERT_IS_TRUE(AssignProcessToJobObject(test_job, test_tester_process.hProcess)); + ASSERT_ARE_NOT_EQUAL(uint32_t, (uint32_t)-1, (uint32_t)ResumeThread(test_tester_process.hThread)); +} + +typedef struct UNIT_OWNER_THREAD_CONTEXT_TAG +{ + char mutex_name[128]; + HANDLE owned_event; + HANDLE exit_event; +} UNIT_OWNER_THREAD_CONTEXT; + +/* owns a mutex of the semaphore and exits without releasing it (no ASSERTs, this is not the test thread) */ +static int unit_owner_thread(void* context) +{ + int result; + UNIT_OWNER_THREAD_CONTEXT* thread_context = context; + HANDLE mutex = CreateMutexA(NULL, FALSE, thread_context->mutex_name); + if (mutex == NULL) + { + result = 1; + } + else + { + if ( + (WaitForSingleObject(mutex, INFINITE) != WAIT_OBJECT_0) || + !SetEvent(thread_context->owned_event) || + (WaitForSingleObject(thread_context->exit_event, INFINITE) != WAIT_OBJECT_0) + ) + { + result = 1; + } + else + { + result = 0; + } + + /* the thread exits while owning the mutex, which abandons it */ + (void)CloseHandle(mutex); + } + + return result; +} + +static void start_unit_owner_thread(UNIT_OWNER_THREAD_CONTEXT* owner_context, THREAD_HANDLE* owner_thread, const char* name, uint32_t unit_index) +{ + ASSERT_IS_TRUE(snprintf(owner_context->mutex_name, sizeof(owner_context->mutex_name), "%s_%" PRIu32 "", name, unit_index) > 0); + owner_context->owned_event = CreateEventA(NULL, TRUE, FALSE, NULL); + ASSERT_IS_NOT_NULL(owner_context->owned_event); + owner_context->exit_event = CreateEventA(NULL, TRUE, FALSE, NULL); + ASSERT_IS_NOT_NULL(owner_context->exit_event); + + ASSERT_ARE_EQUAL(int, THREADAPI_OK, ThreadAPI_Create(owner_thread, unit_owner_thread, owner_context)); + ASSERT_ARE_EQUAL(uint32_t, WAIT_OBJECT_0, (uint32_t)WaitForSingleObject(owner_context->owned_event, TEST_WAIT_TIMEOUT_MS), "the owner thread did not get the unit"); +} + +static void end_unit_owner_thread(UNIT_OWNER_THREAD_CONTEXT* owner_context, THREAD_HANDLE owner_thread) +{ + ASSERT_IS_TRUE(SetEvent(owner_context->exit_event)); + int owner_result; + ASSERT_ARE_EQUAL(int, THREADAPI_OK, ThreadAPI_Join(owner_thread, &owner_result)); + ASSERT_ARE_EQUAL(int, 0, owner_result); + (void)CloseHandle(owner_context->exit_event); + (void)CloseHandle(owner_context->owned_event); +} + +static volatile_atomic int32_t destroyed_from_callback; + +static void on_acquire_complete_destroy(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index) +{ + TEST_USER* test_user = context; + on_acquire_complete(context, result, unit_index); + robust_semaphore_destroy(test_user->robust_semaphore); + test_user->robust_semaphore = NULL; + (void)interlocked_exchange(&destroyed_from_callback, 1); + wake_by_address_all(&destroyed_from_callback); +} + +#define TEST_SLOW_CALLBACK_MS 1000 + +static void on_acquire_complete_slow(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index) +{ + on_acquire_complete(context, result, unit_index); + Sleep(TEST_SLOW_CALLBACK_MS); + (void)interlocked_exchange(&slow_callback_returned, 1); +} + +BEGIN_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) + +TEST_SUITE_INITIALIZE(suite_init) +{ + ASSERT_ARE_EQUAL(int, 0, gballoc_hl_init(NULL, NULL)); +} + +TEST_SUITE_CLEANUP(suite_cleanup) +{ + gballoc_hl_deinit(); +} + +TEST_FUNCTION_INITIALIZE(method_init) +{ + (void)interlocked_exchange(&completed_count, 0); + (void)interlocked_exchange(¤t_holders, 0); + (void)interlocked_exchange(&max_holders, 0); + (void)interlocked_exchange(&destroyed_from_callback, 0); + (void)interlocked_exchange(&slow_callback_returned, 0); + test_job = NULL; + test_held_event = NULL; + (void)memset(&test_tester_process, 0, sizeof(test_tester_process)); + + /* unique per test, so that tests (and parallel test runs) do not share units */ + static volatile_atomic int32_t test_index; + ASSERT_IS_TRUE(snprintf(test_semaphore_name, sizeof(test_semaphore_name), "Local\\robust_semaphore_int_%lu_%" PRId32 "", GetCurrentProcessId(), interlocked_increment(&test_index)) > 0); +} + +/* when a test fails it may leave a tester process and robust semaphore objects behind */ +TEST_FUNCTION_CLEANUP(method_cleanup) +{ + if (test_tester_process.hProcess == NULL) + { + /* do nothing, the test did not start a tester process */ + } + else + { + (void)TerminateProcess(test_tester_process.hProcess, 1); + (void)WaitForSingleObject(test_tester_process.hProcess, TEST_WAIT_TIMEOUT_MS); + (void)CloseHandle(test_tester_process.hThread); + (void)CloseHandle(test_tester_process.hProcess); + (void)memset(&test_tester_process, 0, sizeof(test_tester_process)); + } + + if (test_job == NULL) + { + /* do nothing, the test did not create a job */ + } + else + { + (void)CloseHandle(test_job); + test_job = NULL; + } + + if (test_held_event == NULL) + { + /* do nothing, the test did not create the held event */ + } + else + { + (void)CloseHandle(test_held_event); + test_held_event = NULL; + } + + destroy_test_users(); +} + +/* many users ask for a unit at the same time and only max_count of them get one at once */ +TEST_FUNCTION(at_most_max_count_units_are_held_at_the_same_time) +{ + // arrange + const uint32_t user_count = 6; + const uint32_t max_count = 2; + create_test_users(user_count, test_semaphore_name, max_count); + + // act + for (uint32_t i = 0; i < user_count; i++) + { + acquire_test_user(&test_users[i], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + } + + // assert + uint32_t released_count = 0; + while (released_count < user_count) + { + int32_t expected_acquired = (int32_t)(((max_count + released_count) < user_count) ? (max_count + released_count) : user_count); + wait_for_completed_count(expected_acquired); + + /* nobody else gets a unit until a unit is released */ + Sleep(TEST_NO_MORE_ACQUISITIONS_WAIT_MS); + ASSERT_ARE_EQUAL(int32_t, expected_acquired, interlocked_add(&completed_count, 0)); + ASSERT_IS_TRUE(interlocked_add(¤t_holders, 0) <= (int32_t)max_count); + assert_holders_have_different_units(user_count); + + for (uint32_t i = 0; i < user_count; i++) + { + if (test_user_holds_a_unit(&test_users[i])) + { + ASSERT_IS_TRUE((uint32_t)interlocked_add(&test_users[i].unit_index, 0) < max_count); + release_test_user(&test_users[i]); + released_count++; + break; + } + else + { + /* do nothing, this user waits or already released its unit */ + } + } + } + + ASSERT_ARE_EQUAL(int32_t, (int32_t)max_count, interlocked_add(&max_holders, 0)); + for (uint32_t i = 0; i < user_count; i++) + { + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[i])); + } + wait_for_held_unit_count(test_semaphore_name, max_count, 0); +} + +TEST_FUNCTION(robust_semaphores_with_different_names_do_not_share_units) +{ + // arrange + char other_name[128]; + ASSERT_IS_TRUE(snprintf(other_name, sizeof(other_name), "%s_other", test_semaphore_name) > 0); + create_test_user(&test_users[0], test_semaphore_name, 1, NULL); + create_test_user(&test_users[1], other_name, 1, NULL); + + // act + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + + // cleanup + release_test_user(&test_users[0]); + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); + wait_for_held_unit_count(other_name, 1, 0); +} + +/* the mutexes are opened with the security descriptor by the first user and opened as they are by the other users */ +TEST_FUNCTION(robust_semaphores_created_with_a_security_descriptor_share_units) +{ + // arrange + create_test_user(&test_users[0], test_semaphore_name, 1, "D:P(A;;GA;;;WD)"); + create_test_user(&test_users[1], test_semaphore_name, 1, "D:P(A;;GA;;;WD)"); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[1]); + + // act + release_test_user(&test_users[0]); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + + // cleanup + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(cancel_completes_a_waiting_acquisition_with_CANCELLED_without_taking_a_unit) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[1]); + + // act + robust_semaphore_cancel(test_users[1].robust_semaphore); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, get_result(&test_users[1])); + ASSERT_ARE_EQUAL(uint32_t, ROBUST_SEMAPHORE_UNIT_INDEX_NONE, (uint32_t)interlocked_add(&test_users[1].unit_index, 0)); + wait_for_held_unit_count(test_semaphore_name, 1, 1); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(waiting_longer_than_timeout_ms_completes_with_TIMEOUT_without_taking_a_unit) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + double start = timer_global_get_elapsed_ms(); + + // act + acquire_test_user(&test_users[1], 500, ROBUST_SEMAPHORE_NO_TIMEOUT); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT, get_result(&test_users[1])); + ASSERT_IS_TRUE(interlocked_add_64(&test_users[1].completed_ms, 0) - (int64_t)start >= 450); + wait_for_held_unit_count(test_semaphore_name, 1, 1); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +/* the user never releases its unit, the worker thread releases it after max_hold_ms */ +TEST_FUNCTION(holding_a_unit_longer_than_max_hold_ms_gives_it_to_a_waiting_acquisition) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, 500); + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + + // act + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + ASSERT_IS_TRUE(interlocked_add_64(&test_users[1].completed_ms, 0) - interlocked_add_64(&test_users[0].completed_ms, 0) >= 450); + + // cleanup + /* the unit of user 0 is already released, releasing it again does nothing */ + release_test_user(&test_users[0]); + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +/* max_hold_ms includes the time of the callback, a callback that takes longer gets the unit released as soon as it returns */ +TEST_FUNCTION(a_callback_that_runs_longer_than_max_hold_ms_releases_the_unit_when_it_returns) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user_with_callback(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, 100, on_acquire_complete_slow); + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + + // act + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + ASSERT_ARE_EQUAL(int32_t, 1, interlocked_add(&test_users[1].slow_callback_returned_at_completion, 0), "the unit was released before the callback returned"); + + // cleanup + release_test_user(&test_users[0]); + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +/* the mutexes are owned by threads, a mutex whose owner thread exited is abandoned */ +TEST_FUNCTION(an_abandoned_unit_is_acquired) +{ + // arrange + create_test_users(1, test_semaphore_name, 1); + UNIT_OWNER_THREAD_CONTEXT owner_context; + THREAD_HANDLE owner_thread; + start_unit_owner_thread(&owner_context, &owner_thread, test_semaphore_name, 0); + /* the robust semaphore keeps the mutex open, so it stays abandoned after the owner thread exits */ + end_unit_owner_thread(&owner_context, owner_thread); + + // act + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + + // assert + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + ASSERT_ARE_EQUAL(int32_t, 0, interlocked_add(&test_users[0].unit_index, 0)); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(a_unit_held_by_a_thread_that_exits_without_releasing_it_is_given_to_a_waiting_acquisition) +{ + // arrange + UNIT_OWNER_THREAD_CONTEXT owner_context; + THREAD_HANDLE owner_thread; + start_unit_owner_thread(&owner_context, &owner_thread, test_semaphore_name, 0); + create_test_users(1, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[0]); + + // act + end_unit_owner_thread(&owner_context, owner_thread); + + // assert + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +/* a process that crashes (or is killed) while holding a unit gives it back: an acquisition waiting in another process gets it */ +TEST_FUNCTION(a_unit_held_by_a_process_that_is_killed_is_given_to_an_acquisition_waiting_in_another_process) +{ + // arrange + start_tester_process(test_semaphore_name); + HANDLE tester_wait_handles[2] = { test_held_event, test_tester_process.hProcess }; + ASSERT_ARE_EQUAL(uint32_t, WAIT_OBJECT_0, (uint32_t)WaitForMultipleObjects(2, tester_wait_handles, FALSE, TEST_TESTER_PROCESS_TIMEOUT_MS), "the tester process did not get the unit"); + wait_for_held_unit_count(test_semaphore_name, 1, 1); + + create_test_users(1, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[0]); + + // act + ASSERT_IS_TRUE(TerminateProcess(test_tester_process.hProcess, 42)); + ASSERT_ARE_EQUAL(uint32_t, WAIT_OBJECT_0, (uint32_t)WaitForSingleObject(test_tester_process.hProcess, TEST_WAIT_TIMEOUT_MS)); + + // assert + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + ASSERT_ARE_EQUAL(int32_t, 0, interlocked_add(&test_users[0].unit_index, 0)); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); + /* method_cleanup closes the tester process and the job */ +} + +TEST_FUNCTION(destroying_a_robust_semaphore_that_holds_a_unit_gives_the_unit_to_a_waiting_acquisition) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[1]); + + // act + (void)interlocked_exchange(&test_users[0].released, 1); + (void)interlocked_decrement(¤t_holders); + robust_semaphore_destroy(test_users[0].robust_semaphore); + test_users[0].robust_semaphore = NULL; + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + + // cleanup + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(releasing_a_waiting_acquisition_completes_it_with_CANCELLED_without_taking_a_unit) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[1]); + + // act + robust_semaphore_release(test_users[1].robust_semaphore); + + // assert + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, get_result(&test_users[1])); + wait_for_held_unit_count(test_semaphore_name, 1, 1); + + /* the next acquisition of the same object gets the unit as soon as it is free */ + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + release_test_user(&test_users[0]); + wait_for_completed_count(3); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + + // cleanup + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(destroying_a_robust_semaphore_that_waits_completes_the_acquisition_with_CANCELLED_without_taking_a_unit) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + acquire_test_user(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(1); + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + assert_test_user_waits(&test_users[1]); + + // act + robust_semaphore_destroy(test_users[1].robust_semaphore); + test_users[1].robust_semaphore = NULL; + + // assert + /* the callback is called before robust_semaphore_destroy returns */ + ASSERT_ARE_EQUAL(int32_t, 2, interlocked_add(&completed_count, 0)); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, get_result(&test_users[1])); + wait_for_held_unit_count(test_semaphore_name, 1, 1); + + // cleanup + release_test_user(&test_users[0]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +TEST_FUNCTION(destroying_a_robust_semaphore_from_its_acquire_callback_releases_the_unit) +{ + // arrange + create_test_users(2, test_semaphore_name, 1); + + // act + acquire_test_user_with_callback(&test_users[0], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT, on_acquire_complete_destroy); + + // assert + double start = timer_global_get_elapsed_ms(); + while (interlocked_add(&destroyed_from_callback, 0) == 0) + { + ASSERT_IS_TRUE(timer_global_get_elapsed_ms() - start < TEST_WAIT_TIMEOUT_MS, "the callback did not destroy the robust semaphore"); + (void)wait_on_address(&destroyed_from_callback, 0, 100); + } + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[0])); + (void)interlocked_exchange(&test_users[0].released, 1); + (void)interlocked_decrement(¤t_holders); + + /* the worker thread released the unit after the callback returned */ + acquire_test_user(&test_users[1], ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + wait_for_completed_count(2); + ASSERT_ARE_EQUAL(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_OK, get_result(&test_users[1])); + + // cleanup + release_test_user(&test_users[1]); + wait_for_held_unit_count(test_semaphore_name, 1, 0); +} + +END_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) diff --git a/win32/tests/robust_semaphore_tester/CMakeLists.txt b/win32/tests/robust_semaphore_tester/CMakeLists.txt new file mode 100644 index 00000000..c843505e --- /dev/null +++ b/win32/tests/robust_semaphore_tester/CMakeLists.txt @@ -0,0 +1,18 @@ +#Copyright (c) Microsoft. All rights reserved. +#Licensed under the MIT license. See LICENSE file in the project root for full license information. + +set(robust_semaphore_tester_c_files + robust_semaphore_tester.c +) + +set(robust_semaphore_tester_h_files +) + +add_executable(robust_semaphore_tester ${robust_semaphore_tester_c_files} ${robust_semaphore_tester_h_files}) + +target_link_libraries(robust_semaphore_tester pal_win32 pal_ll_win32) + +set_output_folder_properties(robust_semaphore_tester) + +set_target_properties(robust_semaphore_tester PROPERTIES + FOLDER "tests/c_pal/win32") diff --git a/win32/tests/robust_semaphore_tester/robust_semaphore_tester.c b/win32/tests/robust_semaphore_tester/robust_semaphore_tester.c new file mode 100644 index 00000000..b76af165 --- /dev/null +++ b/win32/tests/robust_semaphore_tester/robust_semaphore_tester.c @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include +#include + +#include "windows.h" + +#include "macro_utils/macro_utils.h" + +#include "c_pal/gballoc_hl.h" + +#include "c_pal/robust_semaphore.h" + +/* the test kills this process long before this */ +#define TESTER_HOLD_MS 120000 + +static void on_acquire_complete(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index) +{ + HANDLE held_event = context; + (void)unit_index; + + if (result != ROBUST_SEMAPHORE_ACQUIRE_OK) + { + (void)printf("Acquiring a unit failed with %" PRI_MU_ENUM "\n", MU_ENUM_VALUE(ROBUST_SEMAPHORE_ACQUIRE_RESULT, result)); + } + else if (!SetEvent(held_event)) + { + (void)printf("SetEvent failed with %lu\n", GetLastError()); + } + else + { + /* the test knows that this process holds the unit */ + } +} + +/* holds a unit of a robust semaphore with 1 unit until it is killed by the test */ +int main(int argc, char* argv[]) +{ + int result; + + if (argc != 3) + { + (void)printf("Usage: robust_semaphore_tester.exe \n"); + result = 1; + } + else if (gballoc_hl_init(NULL, NULL) != 0) + { + (void)printf("gballoc_hl_init failed\n"); + result = 2; + } + else + { + HANDLE held_event = OpenEventA(EVENT_MODIFY_STATE, FALSE, argv[2]); + if (held_event == NULL) + { + (void)printf("OpenEventA(%s) failed with %lu\n", argv[2], GetLastError()); + result = 3; + } + else + { + ROBUST_SEMAPHORE_HANDLE robust_semaphore = robust_semaphore_create(argv[1], 1, NULL); + if (robust_semaphore == NULL) + { + (void)printf("robust_semaphore_create(%s) failed\n", argv[1]); + result = 4; + } + else + { + if (robust_semaphore_acquire_async(robust_semaphore, ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT, on_acquire_complete, held_event) != 0) + { + (void)printf("robust_semaphore_acquire_async failed\n"); + result = 5; + } + else + { + Sleep(TESTER_HOLD_MS); + result = 0; + } + robust_semaphore_destroy(robust_semaphore); + } + (void)CloseHandle(held_event); + } + gballoc_hl_deinit(); + } + + return result; +} diff --git a/win32/tests/robust_semaphore_ut/CMakeLists.txt b/win32/tests/robust_semaphore_ut/CMakeLists.txt new file mode 100644 index 00000000..00775b14 --- /dev/null +++ b/win32/tests/robust_semaphore_ut/CMakeLists.txt @@ -0,0 +1,22 @@ +#Copyright (c) Microsoft. All rights reserved. +#Licensed under the MIT license. See LICENSE file in the project root for full license information. + +set(theseTestsName robust_semaphore_ut) + +set(${theseTestsName}_test_files + ${theseTestsName}.c +) + +set(${theseTestsName}_c_files + robust_semaphore_mocked.c + ../../src/string_utils.c + ../../../c_pal_ll/win32/src/interlocked_win32.c +) + +set(${theseTestsName}_h_files + ../../inc/c_pal/robust_semaphore.h +) + +build_test_artifacts(${theseTestsName} "tests/c_pal/win32" ADDITIONAL_LIBS pal_interfaces pal_ll_interfaces c_pal_reals c_pal_ll_reals + ENABLE_TEST_FILES_PRECOMPILED_HEADERS "${CMAKE_CURRENT_LIST_DIR}/robust_semaphore_ut_pch.h" +) \ No newline at end of file diff --git a/win32/tests/robust_semaphore_ut/robust_semaphore_mocked.c b/win32/tests/robust_semaphore_ut/robust_semaphore_mocked.c new file mode 100644 index 00000000..e078d340 --- /dev/null +++ b/win32/tests/robust_semaphore_ut/robust_semaphore_mocked.c @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "windows.h" +#include "sddl.h" + +#define ConvertStringSecurityDescriptorToSecurityDescriptorA mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA +#define LocalFree mocked_LocalFree +#define CreateMutexExA mocked_CreateMutexExA +#define CreateEventA mocked_CreateEventA +#define CloseHandle mocked_CloseHandle +#define SetEvent mocked_SetEvent +#define ResetEvent mocked_ResetEvent +#define WaitForMultipleObjects mocked_WaitForMultipleObjects +#define WaitForSingleObject mocked_WaitForSingleObject +#define ReleaseMutex mocked_ReleaseMutex +#define GetCurrentThreadId mocked_GetCurrentThreadId + +BOOL WINAPI mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA(LPCSTR StringSecurityDescriptor, DWORD StringSDRevision, PSECURITY_DESCRIPTOR* SecurityDescriptor, PULONG SecurityDescriptorSize); +HLOCAL WINAPI mocked_LocalFree(HLOCAL hMem); +HANDLE WINAPI mocked_CreateMutexExA(LPSECURITY_ATTRIBUTES lpMutexAttributes, LPCSTR lpName, DWORD dwFlags, DWORD dwDesiredAccess); +HANDLE WINAPI mocked_CreateEventA(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCSTR lpName); +BOOL WINAPI mocked_CloseHandle(HANDLE hObject); +BOOL WINAPI mocked_SetEvent(HANDLE hEvent); +BOOL WINAPI mocked_ResetEvent(HANDLE hEvent); +DWORD WINAPI mocked_WaitForMultipleObjects(DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds); +DWORD WINAPI mocked_WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds); +BOOL WINAPI mocked_ReleaseMutex(HANDLE hMutex); +DWORD WINAPI mocked_GetCurrentThreadId(void); + +#include "../../src/robust_semaphore.c" diff --git a/win32/tests/robust_semaphore_ut/robust_semaphore_ut.c b/win32/tests/robust_semaphore_ut/robust_semaphore_ut.c new file mode 100644 index 00000000..9fd83dba --- /dev/null +++ b/win32/tests/robust_semaphore_ut/robust_semaphore_ut.c @@ -0,0 +1,1612 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "robust_semaphore_ut_pch.h" + +#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS +#undef ENABLE_MOCKS_DECL +#include "umock_c/umock_c_prod.h" +MOCKABLE_FUNCTION(WINAPI, BOOL, mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA, LPCSTR, StringSecurityDescriptor, DWORD, StringSDRevision, PSECURITY_DESCRIPTOR*, SecurityDescriptor, PULONG, SecurityDescriptorSize); +MOCKABLE_FUNCTION(WINAPI, HLOCAL, mocked_LocalFree, HLOCAL, hMem); +MOCKABLE_FUNCTION(WINAPI, HANDLE, mocked_CreateMutexExA, LPSECURITY_ATTRIBUTES, lpMutexAttributes, LPCSTR, lpName, DWORD, dwFlags, DWORD, dwDesiredAccess); +MOCKABLE_FUNCTION(WINAPI, HANDLE, mocked_CreateEventA, LPSECURITY_ATTRIBUTES, lpEventAttributes, BOOL, bManualReset, BOOL, bInitialState, LPCSTR, lpName); +MOCKABLE_FUNCTION(WINAPI, BOOL, mocked_CloseHandle, HANDLE, hObject); +MOCKABLE_FUNCTION(WINAPI, BOOL, mocked_SetEvent, HANDLE, hEvent); +MOCKABLE_FUNCTION(WINAPI, BOOL, mocked_ResetEvent, HANDLE, hEvent); +MOCKABLE_FUNCTION(WINAPI, DWORD, mocked_WaitForMultipleObjects, DWORD, nCount, const HANDLE*, lpHandles, BOOL, bWaitAll, DWORD, dwMilliseconds); +MOCKABLE_FUNCTION(WINAPI, DWORD, mocked_WaitForSingleObject, HANDLE, hHandle, DWORD, dwMilliseconds); +MOCKABLE_FUNCTION(WINAPI, BOOL, mocked_ReleaseMutex, HANDLE, hMutex); +MOCKABLE_FUNCTION(WINAPI, DWORD, mocked_GetCurrentThreadId); + +MOCKABLE_FUNCTION(, void, test_on_acquire_complete, void*, context, ROBUST_SEMAPHORE_ACQUIRE_RESULT, result, uint32_t, unit_index); +#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS + +#define TEST_NAME "Global\\test_semaphore" +#define TEST_MAX_COUNT 2 +#define TEST_SECURITY_DESCRIPTOR "D:P(A;;GA;;;SY)(A;;GA;;;BA)" +#define TEST_TIMEOUT_MS 10000 +#define TEST_MAX_HOLD_MS 5000 +#define TEST_WORKER_THREAD_ID 1111 +#define TEST_OTHER_THREAD_ID 2222 + +static PSECURITY_DESCRIPTOR test_security_descriptor = (PSECURITY_DESCRIPTOR)0x4242; +static HANDLE test_work_event = (HANDLE)0x5000; +static HANDLE test_cancel_event = (HANDLE)0x5001; +static HANDLE test_release_event = (HANDLE)0x5002; +static HANDLE test_mutexes[ROBUST_SEMAPHORE_MAX_COUNT]; +static THREAD_HANDLE test_worker_thread = (THREAD_HANDLE)0x7001; +static void* test_context = (void*)0x8001; + +static char expected_mutex_names[ROBUST_SEMAPHORE_MAX_COUNT][64]; + +static THREAD_START_FUNC captured_worker_thread_func; +static void* captured_worker_thread_arg; +static DWORD current_thread_id; + +/* when set, ThreadAPI_Join runs the worker thread function, like a real worker thread that ends while robust_semaphore_destroy waits for it */ +static bool run_worker_thread_in_join; + +static int acquire_async_result_from_callback; + +/* when set, waiting for the work event (without a return value set by the test) starts an acquisition, like another thread would, and then fails */ +static ROBUST_SEMAPHORE_HANDLE acquire_during_work_event_wait; + +/* optionally called from on_acquire_complete, to call the API from the callback like a real user would */ +typedef void (*ON_ACQUIRE_COMPLETE_ACTION)(ROBUST_SEMAPHORE_HANDLE robust_semaphore); +static ON_ACQUIRE_COMPLETE_ACTION on_acquire_complete_action; +static ROBUST_SEMAPHORE_HANDLE on_acquire_complete_action_robust_semaphore; + +TEST_DEFINE_ENUM_TYPE(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); +IMPLEMENT_UMOCK_C_ENUM_TYPE(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT_VALUES); +IMPLEMENT_UMOCK_C_ENUM_TYPE(THREADAPI_RESULT, THREADAPI_RESULT_VALUES); + +MU_DEFINE_ENUM_STRINGS(UMOCK_C_ERROR_CODE, UMOCK_C_ERROR_CODE_VALUES) + +static void on_umock_c_error(UMOCK_C_ERROR_CODE error_code) +{ + ASSERT_FAIL("umock_c reported error :%" PRI_MU_ENUM "", MU_ENUM_VALUE(UMOCK_C_ERROR_CODE, error_code)); +} + +static BOOL WINAPI hook_ConvertStringSecurityDescriptorToSecurityDescriptorA(LPCSTR StringSecurityDescriptor, DWORD StringSDRevision, PSECURITY_DESCRIPTOR* SecurityDescriptor, PULONG SecurityDescriptorSize) +{ + (void)StringSecurityDescriptor; + (void)StringSDRevision; + (void)SecurityDescriptorSize; + *SecurityDescriptor = test_security_descriptor; + return TRUE; +} + +static THREADAPI_RESULT hook_ThreadAPI_Create(THREAD_HANDLE* threadHandle, THREAD_START_FUNC func, void* arg) +{ + captured_worker_thread_func = func; + captured_worker_thread_arg = arg; + *threadHandle = test_worker_thread; + return THREADAPI_OK; +} + +static DWORD WINAPI hook_GetCurrentThreadId(void) +{ + return current_thread_id; +} + +static DWORD WINAPI hook_WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) +{ + (void)dwMilliseconds; + DWORD result; + if ( + (hHandle != test_work_event) || + (acquire_during_work_event_wait == NULL) + ) + { + result = WAIT_OBJECT_0; + } + else + { + ROBUST_SEMAPHORE_HANDLE robust_semaphore = acquire_during_work_event_wait; + acquire_during_work_event_wait = NULL; + ASSERT_ARE_EQUAL(int, 0, robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context)); + result = WAIT_FAILED; + } + return result; +} + +static THREADAPI_RESULT hook_ThreadAPI_Join(THREAD_HANDLE threadHandle, int* res) +{ + (void)threadHandle; + int thread_result = 0; + if (!run_worker_thread_in_join) + { + /* do nothing, the worker thread already ended */ + } + else + { + DWORD saved_thread_id = current_thread_id; + current_thread_id = TEST_WORKER_THREAD_ID; + thread_result = captured_worker_thread_func(captured_worker_thread_arg); + current_thread_id = saved_thread_id; + } + + if (res == NULL) + { + /* do nothing, the caller does not want the thread result */ + } + else + { + *res = thread_result; + } + return THREADAPI_OK; +} + +static void hook_test_on_acquire_complete(void* context, ROBUST_SEMAPHORE_ACQUIRE_RESULT result, uint32_t unit_index) +{ + (void)context; + (void)result; + (void)unit_index; + if (on_acquire_complete_action == NULL) + { + /* do nothing, the test does not call the API from the callback */ + } + else + { + ON_ACQUIRE_COMPLETE_ACTION action = on_acquire_complete_action; + on_acquire_complete_action = NULL; + action(on_acquire_complete_action_robust_semaphore); + } +} + +static void expect_create(const char* security_descriptor) +{ + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_init(IGNORED_ARG)); + if (security_descriptor == NULL) + { + /* do nothing, no security descriptor to convert */ + } + else + { + STRICT_EXPECTED_CALL(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA(security_descriptor, SDDL_REVISION_1, IGNORED_ARG, NULL)); + } + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[i], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(test_mutexes[i]); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + } + if (security_descriptor == NULL) + { + /* do nothing, no security descriptor to free */ + } + else + { + STRICT_EXPECTED_CALL(mocked_LocalFree(test_security_descriptor)) + .CallCannotFail(); + } + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, FALSE, FALSE, NULL)) + .SetReturn(test_work_event); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)) + .SetReturn(test_cancel_event); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)) + .SetReturn(test_release_event); + STRICT_EXPECTED_CALL(ThreadAPI_Create(IGNORED_ARG, IGNORED_ARG, IGNORED_ARG)); +} + +static ROBUST_SEMAPHORE_HANDLE test_create(void) +{ + expect_create(TEST_SECURITY_DESCRIPTOR); + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + ASSERT_IS_NOT_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + umock_c_reset_all_calls(); + return result; +} + +static void expect_free(void) +{ + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_mutexes[i])); + } + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_release_event)); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_cancel_event)); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_deinit(IGNORED_ARG)); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); +} + +/* destroy from a thread other than the worker thread, when there is no acquisition to release */ +static void expect_destroy(void) +{ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(ThreadAPI_Join(test_worker_thread, IGNORED_ARG)); + expect_free(); +} + +static void test_destroy(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + current_thread_id = TEST_OTHER_THREAD_ID; + umock_c_reset_all_calls(); + robust_semaphore_destroy(robust_semaphore); + umock_c_reset_all_calls(); +} + +static void test_acquire_async(ROBUST_SEMAPHORE_HANDLE robust_semaphore, uint32_t timeout_ms, uint32_t max_hold_ms) +{ + ASSERT_ARE_EQUAL(int, 0, robust_semaphore_acquire_async(robust_semaphore, timeout_ms, max_hold_ms, test_on_acquire_complete, test_context)); + umock_c_reset_all_calls(); +} + +static void expect_worker_start(void) +{ + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); +} + +static void expect_worker_takes_acquisition(void) +{ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_ResetEvent(test_cancel_event)); + STRICT_EXPECTED_CALL(mocked_ResetEvent(test_release_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); +} + +static void expect_wait_for_unit(uint32_t timeout_ms, DWORD wait_result) +{ + STRICT_EXPECTED_CALL(mocked_WaitForMultipleObjects(1 + TEST_MAX_COUNT, IGNORED_ARG, FALSE, timeout_ms)) + .SetReturn(wait_result); +} + +static void expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_RESULT acquire_result) +{ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, acquire_result, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); +} + +/* the unit is acquired at 1000 ms; when there is a hold limit, on_acquire_complete returns at callback_returned_ms */ +static void expect_hold_unit(uint32_t unit_index, uint32_t max_hold_ms, double callback_returned_ms, DWORD expected_hold_timeout_ms, DWORD hold_wait_result) +{ + STRICT_EXPECTED_CALL(timer_global_get_elapsed_ms()) + .SetReturn(1000.0); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_OK, unit_index)); + if (max_hold_ms == ROBUST_SEMAPHORE_NO_TIMEOUT) + { + /* do nothing, no time left to compute */ + } + else + { + STRICT_EXPECTED_CALL(timer_global_get_elapsed_ms()) + .SetReturn(callback_returned_ms); + } + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_release_event, expected_hold_timeout_ms)) + .SetReturn(hold_wait_result); + STRICT_EXPECTED_CALL(mocked_ReleaseMutex(test_mutexes[unit_index])); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); +} + +/* the worker finds no new acquisition, waits for the work event, the wait fails and the worker thread ends */ +static void expect_worker_ends_on_work_event_wait_failure(void) +{ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)) + .SetReturn(WAIT_FAILED); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); +} + +static void run_worker_thread(void) +{ + ASSERT_IS_NOT_NULL(captured_worker_thread_func); + current_thread_id = TEST_WORKER_THREAD_ID; + ASSERT_ARE_EQUAL(int, 0, captured_worker_thread_func(captured_worker_thread_arg)); + current_thread_id = TEST_OTHER_THREAD_ID; +} + +/* ends the worker thread (by failing its wait for the work event) and destroys the object */ +static void end_worker_thread_and_destroy(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)) + .SetReturn(WAIT_FAILED); + run_worker_thread(); + test_destroy(robust_semaphore); +} + +static void call_robust_semaphore_acquire_async(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + ASSERT_ARE_EQUAL(int, 0, robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT, test_on_acquire_complete, test_context)); +} + +static void call_robust_semaphore_release(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + robust_semaphore_release(robust_semaphore); +} + +static void call_robust_semaphore_destroy(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + robust_semaphore_destroy(robust_semaphore); +} + +static void call_robust_semaphore_release_then_acquire_async(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + robust_semaphore_release(robust_semaphore); + acquire_async_result_from_callback = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT, test_on_acquire_complete, test_context); +} + +static void call_robust_semaphore_destroy_then_acquire_async(ROBUST_SEMAPHORE_HANDLE robust_semaphore) +{ + robust_semaphore_destroy(robust_semaphore); + acquire_async_result_from_callback = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT, test_on_acquire_complete, test_context); +} + +BEGIN_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) + +TEST_SUITE_INITIALIZE(suite_init) +{ + ASSERT_ARE_EQUAL(int, 0, real_gballoc_hl_init(NULL, NULL)); + + ASSERT_ARE_EQUAL(int, 0, umock_c_init(on_umock_c_error)); + ASSERT_ARE_EQUAL(int, 0, umocktypes_stdint_register_types()); + ASSERT_ARE_EQUAL(int, 0, umocktypes_charptr_register_types()); + ASSERT_ARE_EQUAL(int, 0, umocktypes_windows_register_types()); + + REGISTER_GBALLOC_HL_GLOBAL_MOCK_HOOK(); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(malloc, NULL); + REGISTER_SRW_LOCK_LL_GLOBAL_MOCK_HOOK(); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(srw_lock_ll_init, MU_FAILURE); + + REGISTER_UMOCK_ALIAS_TYPE(PSECURITY_DESCRIPTOR*, void*); + REGISTER_UMOCK_ALIAS_TYPE(PULONG, void*); + REGISTER_UMOCK_ALIAS_TYPE(HLOCAL, void*); + REGISTER_UMOCK_ALIAS_TYPE(const HANDLE*, void*); + REGISTER_UMOCK_ALIAS_TYPE(SRW_LOCK_LL*, void*); + REGISTER_UMOCK_ALIAS_TYPE(THREAD_HANDLE, void*); + REGISTER_UMOCK_ALIAS_TYPE(THREAD_HANDLE*, void*); + REGISTER_UMOCK_ALIAS_TYPE(THREAD_START_FUNC, void*); + REGISTER_UMOCK_ALIAS_TYPE(int*, void*); + REGISTER_TYPE(ROBUST_SEMAPHORE_ACQUIRE_RESULT, ROBUST_SEMAPHORE_ACQUIRE_RESULT); + REGISTER_TYPE(THREADAPI_RESULT, THREADAPI_RESULT); + + REGISTER_GLOBAL_MOCK_HOOK(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA, hook_ConvertStringSecurityDescriptorToSecurityDescriptorA); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA, FALSE); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_LocalFree, NULL, test_security_descriptor); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_CreateMutexExA, (HANDLE)0x6000, NULL); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_CreateEventA, test_work_event, NULL); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_CloseHandle, TRUE, FALSE); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_SetEvent, TRUE, FALSE); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_ResetEvent, TRUE, FALSE); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_WaitForMultipleObjects, WAIT_OBJECT_0, WAIT_FAILED); + REGISTER_GLOBAL_MOCK_HOOK(mocked_WaitForSingleObject, hook_WaitForSingleObject); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(mocked_WaitForSingleObject, WAIT_FAILED); + REGISTER_GLOBAL_MOCK_RETURNS(mocked_ReleaseMutex, TRUE, FALSE); + REGISTER_GLOBAL_MOCK_HOOK(mocked_GetCurrentThreadId, hook_GetCurrentThreadId); + REGISTER_GLOBAL_MOCK_HOOK(ThreadAPI_Create, hook_ThreadAPI_Create); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(ThreadAPI_Create, THREADAPI_ERROR); + REGISTER_GLOBAL_MOCK_HOOK(ThreadAPI_Join, hook_ThreadAPI_Join); + REGISTER_GLOBAL_MOCK_FAIL_RETURN(ThreadAPI_Join, THREADAPI_ERROR); + REGISTER_GLOBAL_MOCK_RETURN(timer_global_get_elapsed_ms, 1000.0); + REGISTER_GLOBAL_MOCK_HOOK(test_on_acquire_complete, hook_test_on_acquire_complete); + + for (uint32_t i = 0; i < ROBUST_SEMAPHORE_MAX_COUNT; i++) + { + test_mutexes[i] = (HANDLE)(uintptr_t)(0x6000 + i); + ASSERT_IS_TRUE(snprintf(expected_mutex_names[i], sizeof(expected_mutex_names[i]), "%s_%" PRIu32 "", TEST_NAME, i) > 0); + } +} + +TEST_SUITE_CLEANUP(suite_cleanup) +{ + umock_c_deinit(); + + real_gballoc_hl_deinit(); +} + +TEST_FUNCTION_INITIALIZE(method_init) +{ + captured_worker_thread_func = NULL; + captured_worker_thread_arg = NULL; + current_thread_id = TEST_OTHER_THREAD_ID; + run_worker_thread_in_join = false; + acquire_during_work_event_wait = NULL; + acquire_async_result_from_callback = 0; + on_acquire_complete_action = NULL; + on_acquire_complete_action_robust_semaphore = NULL; + + umock_c_reset_all_calls(); + umock_c_negative_tests_init(); +} + +TEST_FUNCTION_CLEANUP(method_cleanup) +{ + umock_c_negative_tests_deinit(); +} + +// +// robust_semaphore_create +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_001: [ If name is NULL then robust_semaphore_create shall fail and return NULL. ]*/ +TEST_FUNCTION(robust_semaphore_create_with_NULL_name_fails) +{ + // arrange + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(NULL, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_002: [ If max_count is 0 then robust_semaphore_create shall fail and return NULL. ]*/ +TEST_FUNCTION(robust_semaphore_create_with_0_max_count_fails) +{ + // arrange + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, 0, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_003: [ If max_count is greater than ROBUST_SEMAPHORE_MAX_COUNT then robust_semaphore_create shall fail and return NULL. ]*/ +TEST_FUNCTION(robust_semaphore_create_with_max_count_above_the_limit_fails) +{ + // arrange + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, ROBUST_SEMAPHORE_MAX_COUNT + 1, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_004: [ robust_semaphore_create shall allocate memory for the robust semaphore object. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_005: [ robust_semaphore_create shall initialize a lock by calling srw_lock_ll_init. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_006: [ If security_descriptor is not NULL, robust_semaphore_create shall convert it by calling ConvertStringSecurityDescriptorToSecurityDescriptorA. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_007: [ For each unit i, robust_semaphore_create shall build the mutex name _ by calling sprintf_char. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_008: [ For each unit i, robust_semaphore_create shall create or open the mutex by calling CreateMutexExA with the converted security descriptor, the mutex name and SYNCHRONIZE | MUTEX_MODIFY_STATE access, and free the mutex name. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_009: [ robust_semaphore_create shall free the converted security descriptor by calling LocalFree and log any failure. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_010: [ robust_semaphore_create shall create an auto reset work event, a manual reset cancel event and a manual reset release event by calling CreateEventA. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_011: [ robust_semaphore_create shall start the worker thread by calling ThreadAPI_Create with robust_semaphore_worker_thread. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_012: [ robust_semaphore_create shall succeed and return the robust semaphore object. ]*/ +TEST_FUNCTION(robust_semaphore_create_succeeds) +{ + // arrange + expect_create(TEST_SECURITY_DESCRIPTOR); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NOT_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + ASSERT_IS_NOT_NULL(captured_worker_thread_func); + ASSERT_ARE_EQUAL(void_ptr, result, captured_worker_thread_arg); + + // cleanup + end_worker_thread_and_destroy(result); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_006: [ If security_descriptor is not NULL, robust_semaphore_create shall convert it by calling ConvertStringSecurityDescriptorToSecurityDescriptorA. ]*/ +TEST_FUNCTION(robust_semaphore_create_with_NULL_security_descriptor_succeeds) +{ + // arrange + expect_create(NULL); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, NULL); + + // assert + ASSERT_IS_NOT_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(result); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_007: [ For each unit i, robust_semaphore_create shall build the mutex name _ by calling sprintf_char. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_008: [ For each unit i, robust_semaphore_create shall create or open the mutex by calling CreateMutexExA with the converted security descriptor, the mutex name and SYNCHRONIZE | MUTEX_MODIFY_STATE access, and free the mutex name. ]*/ +TEST_FUNCTION(robust_semaphore_create_with_ROBUST_SEMAPHORE_MAX_COUNT_creates_all_the_mutexes) +{ + // arrange + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_init(IGNORED_ARG)); + for (uint32_t i = 0; i < ROBUST_SEMAPHORE_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[i], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(test_mutexes[i]); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + } + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, FALSE, FALSE, NULL)); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)); + STRICT_EXPECTED_CALL(ThreadAPI_Create(IGNORED_ARG, IGNORED_ARG, IGNORED_ARG)); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, ROBUST_SEMAPHORE_MAX_COUNT, NULL); + + // assert + ASSERT_IS_NOT_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(IGNORED_ARG, INFINITE)) + .SetReturn(WAIT_FAILED); + run_worker_thread(); + robust_semaphore_destroy(result); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_009: [ robust_semaphore_create shall free the converted security descriptor by calling LocalFree and log any failure. ]*/ +TEST_FUNCTION(robust_semaphore_create_succeeds_when_LocalFree_fails) +{ + // arrange + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_init(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA(TEST_SECURITY_DESCRIPTOR, SDDL_REVISION_1, IGNORED_ARG, NULL)); + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[i], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(test_mutexes[i]); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + } + STRICT_EXPECTED_CALL(mocked_LocalFree(test_security_descriptor)) + .SetReturn(test_security_descriptor); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, FALSE, FALSE, NULL)); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)); + STRICT_EXPECTED_CALL(ThreadAPI_Create(IGNORED_ARG, IGNORED_ARG, IGNORED_ARG)); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NOT_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(result); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ +TEST_FUNCTION(when_underlying_calls_fail_robust_semaphore_create_fails) +{ + // arrange + expect_create(TEST_SECURITY_DESCRIPTOR); + umock_c_negative_tests_snapshot(); + + for (size_t i = 0; i < umock_c_negative_tests_call_count(); i++) + { + if (umock_c_negative_tests_can_call_fail(i)) + { + umock_c_negative_tests_reset(); + umock_c_negative_tests_fail_call(i); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result, "On failed call %zu", i); + } + else + { + /* do nothing, this call cannot fail */ + } + } +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ +TEST_FUNCTION(when_ThreadAPI_Create_fails_robust_semaphore_create_closes_all_the_handles_even_when_CloseHandle_fails) +{ + // arrange + expect_create(TEST_SECURITY_DESCRIPTOR); + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_init(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA(TEST_SECURITY_DESCRIPTOR, SDDL_REVISION_1, IGNORED_ARG, NULL)); + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[i], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(test_mutexes[i]); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + } + STRICT_EXPECTED_CALL(mocked_LocalFree(test_security_descriptor)); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, FALSE, FALSE, NULL)) + .SetReturn(test_work_event); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)) + .SetReturn(test_cancel_event); + STRICT_EXPECTED_CALL(mocked_CreateEventA(NULL, TRUE, FALSE, NULL)) + .SetReturn(test_release_event); + STRICT_EXPECTED_CALL(ThreadAPI_Create(IGNORED_ARG, IGNORED_ARG, IGNORED_ARG)) + .SetReturn(THREADAPI_ERROR); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_release_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_cancel_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_work_event)) + .SetReturn(FALSE); + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_mutexes[i])) + .SetReturn(FALSE); + } + STRICT_EXPECTED_CALL(srw_lock_ll_deinit(IGNORED_ARG)); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_013: [ If there are any failures then robust_semaphore_create shall close the handles it created by calling CloseHandle (logging any failure), free the memory it allocated and fail and return NULL. ]*/ +TEST_FUNCTION(when_CreateMutexExA_fails_for_the_second_unit_robust_semaphore_create_closes_the_first_mutex) +{ + // arrange + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_init(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_ConvertStringSecurityDescriptorToSecurityDescriptorA(TEST_SECURITY_DESCRIPTOR, SDDL_REVISION_1, IGNORED_ARG, NULL)); + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[0], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(test_mutexes[0]); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + STRICT_EXPECTED_CALL(malloc(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CreateMutexExA(IGNORED_ARG, expected_mutex_names[1], 0, SYNCHRONIZE | MUTEX_MODIFY_STATE)) + .SetReturn(NULL); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_LocalFree(test_security_descriptor)); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_mutexes[0])); + STRICT_EXPECTED_CALL(srw_lock_ll_deinit(IGNORED_ARG)); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + + // act + ROBUST_SEMAPHORE_HANDLE result = robust_semaphore_create(TEST_NAME, TEST_MAX_COUNT, TEST_SECURITY_DESCRIPTOR); + + // assert + ASSERT_IS_NULL(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +// +// robust_semaphore_destroy +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_014: [ If robust_semaphore is NULL then robust_semaphore_destroy shall return. ]*/ +TEST_FUNCTION(robust_semaphore_destroy_with_NULL_returns) +{ + // arrange + + // act + robust_semaphore_destroy(NULL); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_015: [ robust_semaphore_destroy shall acquire the lock in exclusive mode. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_016: [ robust_semaphore_destroy shall mark the object as being destroyed, release the current acquisition as robust_semaphore_release does and wake the worker thread by calling SetEvent on the work event. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_018: [ Otherwise robust_semaphore_destroy shall release the lock and wait for the worker thread to end by calling ThreadAPI_Join. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_019: [ robust_semaphore_destroy shall close the mutexes and the events by calling CloseHandle (logging any failure), deinitialize the lock and free the robust semaphore object. ]*/ +TEST_FUNCTION(robust_semaphore_destroy_wakes_waits_for_and_frees) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + expect_destroy(); + + // act + robust_semaphore_destroy(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_016: [ robust_semaphore_destroy shall mark the object as being destroyed, release the current acquisition as robust_semaphore_release does and wake the worker thread by calling SetEvent on the work event. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_048: [ If there is no new acquisition and the object is being destroyed, robust_semaphore_worker_thread shall return 0. ]*/ +TEST_FUNCTION(robust_semaphore_destroy_cancels_a_pending_acquisition_and_the_worker_thread_completes_it_and_ends) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + run_worker_thread_in_join = true; + + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_release_event)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(ThreadAPI_Join(test_worker_thread, IGNORED_ARG)); + /* the worker thread completes the released acquisition with CANCELLED */ + expect_worker_start(); + expect_worker_takes_acquisition(); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + /* and ends because the object is being destroyed */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + expect_free(); + + // act + robust_semaphore_destroy(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_053: [ If ThreadAPI_Join fails, robust_semaphore_destroy shall log an error and return without freeing the object, because the worker thread may still use it. ]*/ +TEST_FUNCTION(when_ThreadAPI_Join_fails_robust_semaphore_destroy_does_not_free_the_object) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(ThreadAPI_Join(test_worker_thread, IGNORED_ARG)) + .SetReturn(THREADAPI_ERROR); + + // act + robust_semaphore_destroy(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + /* destroying again joins successfully and frees the object */ + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_019: [ robust_semaphore_destroy shall close the mutexes and the events by calling CloseHandle (logging any failure), deinitialize the lock and free the robust semaphore object. ]*/ +TEST_FUNCTION(robust_semaphore_destroy_frees_the_object_even_when_CloseHandle_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(ThreadAPI_Join(test_worker_thread, IGNORED_ARG)); + for (uint32_t i = 0; i < TEST_MAX_COUNT; i++) + { + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_mutexes[i])) + .SetReturn(FALSE); + } + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_release_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_cancel_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_work_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_deinit(IGNORED_ARG)); + STRICT_EXPECTED_CALL(free(IGNORED_ARG)); + + // act + robust_semaphore_destroy(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_017: [ If robust_semaphore_destroy is called on the worker thread, it shall log an error, mark the worker thread as the one that frees the object, release the lock and return. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_049: [ If robust_semaphore_destroy was called on the worker thread, before returning robust_semaphore_worker_thread shall close the mutexes, the events and its own thread handle by calling CloseHandle (logging any failure), deinitialize the lock and free the robust semaphore object. ]*/ +TEST_FUNCTION(robust_semaphore_destroy_called_from_on_acquire_complete_lets_the_worker_thread_free_the_object) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + on_acquire_complete_action = call_robust_semaphore_destroy; + on_acquire_complete_action_robust_semaphore = robust_semaphore; + + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_TIMEOUT); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + /* destroy, called from the callback on the worker thread */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* the worker thread sees the object is being destroyed and frees it */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_worker_thread)); + expect_free(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +// +// robust_semaphore_acquire_async +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_020: [ If robust_semaphore is NULL then robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_with_NULL_robust_semaphore_fails) +{ + // arrange + + // act + int result = robust_semaphore_acquire_async(NULL, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_021: [ If on_acquire_complete is NULL then robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_with_NULL_on_acquire_complete_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + + // act + int result = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, NULL, test_context); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_022: [ robust_semaphore_acquire_async shall acquire the lock in exclusive mode. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_025: [ robust_semaphore_acquire_async shall store timeout_ms, max_hold_ms, on_acquire_complete and on_acquire_complete_context as the new current acquisition and wake the worker thread by calling SetEvent on the work event. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_026: [ robust_semaphore_acquire_async shall release the lock and succeed and return 0. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_succeeds) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + int result = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context); + + // assert + ASSERT_ARE_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + umock_c_reset_all_calls(); + robust_semaphore_cancel(robust_semaphore); + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_024: [ If there is a current acquisition that was not released or ended, robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_while_an_acquisition_is_in_progress_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + int result = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + umock_c_reset_all_calls(); + robust_semaphore_cancel(robust_semaphore); + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_027: [ If SetEvent fails, robust_semaphore_acquire_async shall forget the new acquisition, release the lock and fail and return a non-zero value. ]*/ +TEST_FUNCTION(when_SetEvent_fails_robust_semaphore_acquire_async_fails_and_a_new_acquisition_can_be_started) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + int result = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + ASSERT_ARE_EQUAL(int, 0, robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context)); + + // cleanup + umock_c_reset_all_calls(); + robust_semaphore_cancel(robust_semaphore); + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_023: [ If the object is being destroyed or its worker thread ended, robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_after_the_worker_thread_ended_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)) + .SetReturn(WAIT_FAILED); + run_worker_thread(); + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + int result = robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_023: [ If the object is being destroyed or its worker thread ended, robust_semaphore_acquire_async shall fail and return a non-zero value. ]*/ +TEST_FUNCTION(robust_semaphore_acquire_async_while_the_object_is_being_destroyed_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, 0, ROBUST_SEMAPHORE_NO_TIMEOUT); + /* destroy on the worker thread marks the object as being destroyed without freeing it */ + on_acquire_complete_action = call_robust_semaphore_destroy_then_acquire_async; + on_acquire_complete_action_robust_semaphore = robust_semaphore; + STRICT_EXPECTED_CALL(mocked_WaitForMultipleObjects(IGNORED_ARG, IGNORED_ARG, IGNORED_ARG, IGNORED_ARG)) + .SetReturn(WAIT_TIMEOUT); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_NOT_EQUAL(int, 0, acquire_async_result_from_callback); +} + +// +// robust_semaphore_cancel +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_028: [ If robust_semaphore is NULL then robust_semaphore_cancel shall return. ]*/ +TEST_FUNCTION(robust_semaphore_cancel_with_NULL_returns) +{ + // arrange + + // act + robust_semaphore_cancel(NULL); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_029: [ robust_semaphore_cancel shall acquire the lock in exclusive mode. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_030: [ If the current acquisition was not released and did not get a unit, robust_semaphore_cancel shall mark it as cancelled and signal the cancel event by calling SetEvent, logging any failure. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_031: [ robust_semaphore_cancel shall release the lock. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_038: [ If the acquisition was already cancelled or released, robust_semaphore_worker_thread shall end it and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED and ROBUST_SEMAPHORE_UNIT_INDEX_NONE without waiting for a unit. ]*/ +TEST_FUNCTION(robust_semaphore_cancel_of_an_acquisition_not_taken_yet_completes_it_with_CANCELLED_without_waiting) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_cancel(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + umock_c_reset_all_calls(); + expect_worker_start(); + expect_worker_takes_acquisition(); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + expect_worker_ends_on_work_event_wait_failure(); + run_worker_thread(); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_030: [ If the current acquisition was not released and did not get a unit, robust_semaphore_cancel shall mark it as cancelled and signal the cancel event by calling SetEvent, logging any failure. ]*/ +TEST_FUNCTION(robust_semaphore_cancel_without_an_acquisition_does_nothing) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_cancel(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_030: [ If the current acquisition was not released and did not get a unit, robust_semaphore_cancel shall mark it as cancelled and signal the cancel event by calling SetEvent, logging any failure. ]*/ +TEST_FUNCTION(robust_semaphore_cancel_when_SetEvent_fails_still_cancels) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_cancel(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + /* the acquisition is still marked as cancelled */ + umock_c_reset_all_calls(); + expect_worker_start(); + expect_worker_takes_acquisition(); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + expect_worker_ends_on_work_event_wait_failure(); + run_worker_thread(); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +// +// robust_semaphore_release +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_032: [ If robust_semaphore is NULL then robust_semaphore_release shall return. ]*/ +TEST_FUNCTION(robust_semaphore_release_with_NULL_returns) +{ + // arrange + + // act + robust_semaphore_release(NULL); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_033: [ robust_semaphore_release shall acquire the lock in exclusive mode. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_034: [ If there is a current acquisition that was not released, robust_semaphore_release shall mark it as released and signal the cancel event and the release event by calling SetEvent, logging any failure. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_035: [ robust_semaphore_release shall release the lock. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_038: [ If the acquisition was already cancelled or released, robust_semaphore_worker_thread shall end it and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED and ROBUST_SEMAPHORE_UNIT_INDEX_NONE without waiting for a unit. ]*/ +TEST_FUNCTION(robust_semaphore_release_of_an_acquisition_not_taken_yet_completes_it_with_CANCELLED) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_release_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_release(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + /* the waiting acquisition ends only when the worker thread completes it */ + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + ASSERT_ARE_NOT_EQUAL(int, 0, robust_semaphore_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS, test_on_acquire_complete, test_context)); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + umock_c_reset_all_calls(); + expect_worker_start(); + expect_worker_takes_acquisition(); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_CANCELLED, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + expect_worker_ends_on_work_event_wait_failure(); + run_worker_thread(); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_034: [ If there is a current acquisition that was not released, robust_semaphore_release shall mark it as released and signal the cancel event and the release event by calling SetEvent, logging any failure. ]*/ +TEST_FUNCTION(robust_semaphore_release_twice_signals_the_events_once) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + robust_semaphore_release(robust_semaphore); + umock_c_reset_all_calls(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_release(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_034: [ If there is a current acquisition that was not released, robust_semaphore_release shall mark it as released and signal the cancel event and the release event by calling SetEvent, logging any failure. ]*/ +TEST_FUNCTION(robust_semaphore_release_without_an_acquisition_does_nothing) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + robust_semaphore_release(robust_semaphore); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + end_worker_thread_and_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_051: [ If the current acquisition holds a unit, robust_semaphore_release shall end it. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_047: [ If a unit was acquired, robust_semaphore_worker_thread shall release the mutex by calling ReleaseMutex (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. ]*/ +TEST_FUNCTION(releasing_a_held_unit_from_on_acquire_complete_ends_the_acquisition_and_a_new_one_can_start_right_away) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + on_acquire_complete_action = call_robust_semaphore_release_then_acquire_async; + on_acquire_complete_action_robust_semaphore = robust_semaphore; + + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + STRICT_EXPECTED_CALL(timer_global_get_elapsed_ms()); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_OK, 0)); + /* release, then acquire again, from the callback */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_release_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* the worker thread releases the first unit without touching the new acquisition */ + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_release_event, INFINITE)); + STRICT_EXPECTED_CALL(mocked_ReleaseMutex(test_mutexes[0])); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* and then takes the new acquisition */ + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_TIMEOUT); + expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + ASSERT_ARE_EQUAL(int, 0, acquire_async_result_from_callback); + + // cleanup + test_destroy(robust_semaphore); +} + +// +// robust_semaphore_worker_thread +// + +/*Tests_SRS_ROBUST_SEMAPHORE_01_036: [ robust_semaphore_worker_thread shall record the id of the worker thread by calling GetCurrentThreadId. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_037: [ If there is a new acquisition, robust_semaphore_worker_thread shall take it and reset the cancel event and the release event by calling ResetEvent, logging any failure. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_039: [ Otherwise robust_semaphore_worker_thread shall wait for the cancel event or any of the mutexes by calling WaitForMultipleObjects with the cancel event followed by the max_count mutexes, bWaitAll FALSE and timeout_ms as timeout (INFINITE if timeout_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_041: [ If WaitForMultipleObjects returns WAIT_OBJECT_0 + 1 + i or WAIT_ABANDONED_0 + 1 + i then robust_semaphore_worker_thread shall record the time the unit was acquired by calling timer_global_get_elapsed_ms, mark the acquisition as holding unit i and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_OK and i. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_045: [ If a unit was acquired, after on_acquire_complete returns, robust_semaphore_worker_thread shall wait for the release event by calling WaitForSingleObject with what is left of max_hold_ms since the unit was acquired (measured by calling timer_global_get_elapsed_ms, 0 if max_hold_ms already passed, INFINITE if max_hold_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_047: [ If a unit was acquired, robust_semaphore_worker_thread shall release the mutex by calling ReleaseMutex (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_052: [ If there is no new acquisition and the object is not being destroyed, robust_semaphore_worker_thread shall wait for the work event by calling WaitForSingleObject with INFINITE timeout and then check again for a new acquisition. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_acquires_unit_1_and_holds_it_until_released) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, ROBUST_SEMAPHORE_NO_TIMEOUT, ROBUST_SEMAPHORE_NO_TIMEOUT); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(INFINITE, WAIT_OBJECT_0 + 2); + expect_hold_unit(1, ROBUST_SEMAPHORE_NO_TIMEOUT, 0, INFINITE, WAIT_OBJECT_0); + /* nothing to do: the worker thread waits for the work event, is woken up and checks again */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)) + .SetReturn(WAIT_OBJECT_0); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_041: [ If WaitForMultipleObjects returns WAIT_OBJECT_0 + 1 + i or WAIT_ABANDONED_0 + 1 + i then robust_semaphore_worker_thread shall record the time the unit was acquired by calling timer_global_get_elapsed_ms, mark the acquisition as holding unit i and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_OK and i. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_acquires_an_abandoned_unit) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_ABANDONED_0 + 2); + expect_hold_unit(1, ROBUST_SEMAPHORE_NO_TIMEOUT, 0, INFINITE, WAIT_OBJECT_0); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_037: [ If there is a new acquisition, robust_semaphore_worker_thread shall take it and reset the cancel event and the release event by calling ResetEvent, logging any failure. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_waits_for_a_unit_even_when_ResetEvent_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + expect_worker_start(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_ResetEvent(test_cancel_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(mocked_ResetEvent(test_release_event)) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + expect_hold_unit(0, ROBUST_SEMAPHORE_NO_TIMEOUT, 0, INFINITE, WAIT_OBJECT_0); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_040: [ If WaitForMultipleObjects returns WAIT_OBJECT_0 then robust_semaphore_worker_thread shall end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_CANCELLED and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_completes_with_CANCELLED_when_the_cancel_event_is_signaled) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0); + expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_CANCELLED); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_042: [ If WaitForMultipleObjects returns WAIT_TIMEOUT then robust_semaphore_worker_thread shall end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_completes_with_TIMEOUT_when_no_unit_is_free_within_timeout_ms) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, 0, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(0, WAIT_TIMEOUT); + expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_043: [ If WaitForMultipleObjects returns any other value then robust_semaphore_worker_thread shall log the error, end the acquisition and call on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_ERROR and ROBUST_SEMAPHORE_UNIT_INDEX_NONE. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_completes_with_ERROR_when_WaitForMultipleObjects_fails) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_FAILED); + expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_ERROR); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_044: [ robust_semaphore_worker_thread shall end an acquisition that did not get a unit before calling its on_acquire_complete, so that on_acquire_complete can start a new acquisition. ]*/ +TEST_FUNCTION(on_acquire_complete_of_an_acquisition_that_timed_out_can_start_a_new_acquisition) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + on_acquire_complete_action = call_robust_semaphore_acquire_async; + on_acquire_complete_action_robust_semaphore = robust_semaphore; + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_TIMEOUT); + expect_end_without_unit(ROBUST_SEMAPHORE_ACQUIRE_TIMEOUT); + /* the new acquisition, started from the callback */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* is taken right away */ + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + expect_hold_unit(0, ROBUST_SEMAPHORE_NO_TIMEOUT, 0, INFINITE, WAIT_OBJECT_0); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_045: [ If a unit was acquired, after on_acquire_complete returns, robust_semaphore_worker_thread shall wait for the release event by calling WaitForSingleObject with what is left of max_hold_ms since the unit was acquired (measured by calling timer_global_get_elapsed_ms, 0 if max_hold_ms already passed, INFINITE if max_hold_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_holds_the_unit_for_what_is_left_of_max_hold_ms_after_on_acquire_complete_returns) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + /* acquired at 1000 ms, on_acquire_complete returns at 3000 ms, 5000 - 2000 = 3000 ms left */ + expect_hold_unit(0, TEST_MAX_HOLD_MS, 3000.0, 3000, WAIT_OBJECT_0); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_045: [ If a unit was acquired, after on_acquire_complete returns, robust_semaphore_worker_thread shall wait for the release event by calling WaitForSingleObject with what is left of max_hold_ms since the unit was acquired (measured by calling timer_global_get_elapsed_ms, 0 if max_hold_ms already passed, INFINITE if max_hold_ms is ROBUST_SEMAPHORE_NO_TIMEOUT). ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_046: [ If waiting for the release event times out then robust_semaphore_worker_thread shall log an error and release the mutex early by calling ReleaseMutex. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_releases_the_unit_right_away_when_on_acquire_complete_runs_longer_than_max_hold_ms) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + /* acquired at 1000 ms, on_acquire_complete returns at 7000 ms, max_hold_ms already passed */ + expect_hold_unit(0, TEST_MAX_HOLD_MS, 7000.0, 0, WAIT_TIMEOUT); + expect_worker_ends_on_work_event_wait_failure(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_046: [ If waiting for the release event times out then robust_semaphore_worker_thread shall log an error and release the mutex early by calling ReleaseMutex. ]*/ +/*Tests_SRS_ROBUST_SEMAPHORE_01_047: [ If a unit was acquired, robust_semaphore_worker_thread shall release the mutex by calling ReleaseMutex (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_releases_the_unit_early_when_max_hold_ms_expires_and_a_new_acquisition_can_start) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, TEST_MAX_HOLD_MS); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + expect_hold_unit(0, TEST_MAX_HOLD_MS, 1000.0, TEST_MAX_HOLD_MS, WAIT_TIMEOUT); + /* the acquisition ended, so a new one can be started (the work event wait then fails and the worker thread ends) */ + acquire_during_work_event_wait = robust_semaphore; + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_ERROR, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_047: [ If a unit was acquired, robust_semaphore_worker_thread shall release the mutex by calling ReleaseMutex (logging any failure) and, if the acquisition still holds the unit (it was not released), end it. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_ends_the_acquisition_even_when_ReleaseMutex_and_the_release_wait_fail) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + STRICT_EXPECTED_CALL(timer_global_get_elapsed_ms()); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_OK, 0)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_release_event, INFINITE)) + .SetReturn(WAIT_FAILED); + STRICT_EXPECTED_CALL(mocked_ReleaseMutex(test_mutexes[0])) + .SetReturn(FALSE); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* the acquisition ended, so a new one can be started (the work event wait then fails and the worker thread ends) */ + acquire_during_work_event_wait = robust_semaphore; + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_ERROR, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_050: [ If WaitForSingleObject on the work event fails, robust_semaphore_worker_thread shall log the error, mark the worker thread as ended, end a new acquisition it did not take yet and call its on_acquire_complete with ROBUST_SEMAPHORE_ACQUIRE_ERROR and ROBUST_SEMAPHORE_UNIT_INDEX_NONE, and return 0. ]*/ +TEST_FUNCTION(when_waiting_for_the_work_event_fails_robust_semaphore_worker_thread_completes_a_new_acquisition_with_ERROR_and_ends) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + acquire_during_work_event_wait = robust_semaphore; + expect_worker_start(); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_work_event, INFINITE)); + /* robust_semaphore_acquire_async from another thread while the worker thread waits */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_ERROR, ROBUST_SEMAPHORE_UNIT_INDEX_NONE)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + + // cleanup + test_destroy(robust_semaphore); +} + +/*Tests_SRS_ROBUST_SEMAPHORE_01_048: [ If there is no new acquisition and the object is being destroyed, robust_semaphore_worker_thread shall return 0. ]*/ +TEST_FUNCTION(robust_semaphore_worker_thread_releases_a_held_unit_and_ends_when_the_object_is_destroyed) +{ + // arrange + ROBUST_SEMAPHORE_HANDLE robust_semaphore = test_create(); + test_acquire_async(robust_semaphore, TEST_TIMEOUT_MS, ROBUST_SEMAPHORE_NO_TIMEOUT); + /* destroyed while the unit is held: from on_acquire_complete, the only place where it can happen when the worker thread runs on the test thread */ + on_acquire_complete_action = call_robust_semaphore_destroy; + on_acquire_complete_action_robust_semaphore = robust_semaphore; + + expect_worker_start(); + expect_worker_takes_acquisition(); + expect_wait_for_unit(TEST_TIMEOUT_MS, WAIT_OBJECT_0 + 1); + STRICT_EXPECTED_CALL(timer_global_get_elapsed_ms()); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(test_on_acquire_complete(test_context, ROBUST_SEMAPHORE_ACQUIRE_OK, 0)); + /* destroy releases the held unit */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_cancel_event)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_release_event)); + STRICT_EXPECTED_CALL(mocked_SetEvent(test_work_event)); + STRICT_EXPECTED_CALL(mocked_GetCurrentThreadId()); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* the worker thread releases the mutex */ + STRICT_EXPECTED_CALL(mocked_WaitForSingleObject(test_release_event, INFINITE)); + STRICT_EXPECTED_CALL(mocked_ReleaseMutex(test_mutexes[0])); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + /* finds no new acquisition, the object is being destroyed: it ends and frees the object */ + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_acquire_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(srw_lock_ll_release_exclusive(IGNORED_ARG)); + STRICT_EXPECTED_CALL(mocked_CloseHandle(test_worker_thread)); + expect_free(); + + // act + run_worker_thread(); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); +} + +END_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) diff --git a/win32/tests/robust_semaphore_ut/robust_semaphore_ut_pch.h b/win32/tests/robust_semaphore_ut/robust_semaphore_ut_pch.h new file mode 100644 index 00000000..53840e82 --- /dev/null +++ b/win32/tests/robust_semaphore_ut/robust_semaphore_ut_pch.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Precompiled header for robust_semaphore_ut + +#ifndef ROBUST_SEMAPHORE_UT_PCH_H +#define ROBUST_SEMAPHORE_UT_PCH_H + +#include +#include +#include +#include +#include + +#include "windows.h" +#include "sddl.h" + +#include "macro_utils/macro_utils.h" + +#include "real_gballoc_ll.h" + +#include "testrunnerswitcher.h" +#include "umock_c/umock_c.h" +#include "umock_c/umocktypes_stdint.h" +#include "umock_c/umocktypes_charptr.h" +#include "umock_c/umocktypes_windows.h" +#include "umock_c/umock_c_negative_tests.h" + +#include "c_pal/interlocked.h" + +#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS + +#include "c_pal/gballoc_hl.h" +#include "c_pal/gballoc_hl_redirect.h" +#include "c_pal/srw_lock_ll.h" +#include "c_pal/threadapi.h" +#include "c_pal/timer.h" + +#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS + +#include "real_gballoc_hl.h" +#include "real_srw_lock_ll.h" + +#include "c_pal/string_utils.h" + +#include "c_pal/robust_semaphore.h" + +#endif // ROBUST_SEMAPHORE_UT_PCH_H