// 23 april 2019 #define UNICODE #define _UNICODE #define STRICT #define STRICT_TYPED_ITEMIDS #define WINVER 0x0600 #define _WIN32_WINNT 0x0600 #define _WIN32_WINDOWS 0x0600 #define _WIN32_IE 0x0700 #define NTDDI_VERSION 0x06000000 #include #include #include #include #include #include #include #include "testing.h" #include "testingpriv.h" static HRESULT lastErrorCodeToHRESULT(DWORD lastError) { if (lastError == 0) return E_FAIL; return HRESULT_FROM_WIN32(lastError); } static HRESULT lastErrorToHRESULT(void) { return lastErrorCodeToHRESULT(GetLastError()); } static HRESULT WINAPI hrWaitForMultipleObjectsEx(DWORD n, const HANDLE *objects, BOOL waitAll, DWORD timeout, BOOL alertable, DWORD *result) { SetLastError(0); *result = WaitForMultipleObjectsEx(n, objects, waitAll, timeout, alertable); if (*result == WAIT_FAILED) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrSuspendThread(HANDLE thread) { DWORD ret; SetLastError(0); ret = SuspendThread(thread); if (ret == (DWORD) (-1)) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrGetThreadContext(HANDLE thread, LPCONTEXT ctx) { BOOL ret; SetLastError(0); ret = GetThreadContext(thread, ctx); if (ret == 0) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrSetThreadContext(HANDLE thread, CONST CONTEXT *ctx) { BOOL ret; SetLastError(0); ret = SetThreadContext(thread, ctx); if (ret == 0) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrResumeThread(HANDLE thread) { DWORD ret; SetLastError(0); ret = ResumeThread(thread); if (ret == (DWORD) (-1)) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrDuplicateHandle(HANDLE sourceProcess, HANDLE sourceHandle, HANDLE targetProcess, LPHANDLE targetHandle, DWORD access, BOOL inherit, DWORD options) { BOOL ret; SetLastError(0); ret = DuplicateHandle(sourceProcess, sourceHandle, targetProcess, targetHandle, access, inherit, options); if (ret == 0) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrCreateWaitableTimerW(LPSECURITY_ATTRIBUTES attributes, BOOL manualReset, LPCWSTR name, HANDLE *handle) { SetLastError(0); *handle = CreateWaitableTimerW(attributes, manualReset, name); if (*handle == NULL) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrCreateEventW(LPSECURITY_ATTRIBUTES attributes, BOOL manualReset, BOOL initialState, LPCWSTR name, HANDLE *handle) { SetLastError(0); *handle = CreateEventW(attributes, manualReset, initialState, name); if (*handle == NULL) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrSetWaitableTimer(HANDLE timer, const LARGE_INTEGER *duration, LONG period, PTIMERAPCROUTINE completionRoutine, LPVOID completionData, BOOL resume) { BOOL ret; SetLastError(0); ret = SetWaitableTimer(timer, duration, period, completionRoutine, completionData, resume); if (ret == 0) return lastErrorToHRESULT(); return S_OK; } static HRESULT __cdecl hr_beginthreadex(void *security, unsigned stackSize, unsigned (__stdcall *threadProc)(void *arg), void *threadProcArg, unsigned flags, unsigned *thirdArg, uintptr_t *handle) { DWORD lastError; // _doserrno is the equivalent of GetLastError(), or at least that's how _beginthreadex() uses it. _doserrno = 0; *handle = _beginthreadex(security, stackSize, threadProc, threadProcArg, flags, thirdArg); if (*handle == 0) { lastError = (DWORD) _doserrno; return lastErrorCodeToHRESULT(lastError); } return S_OK; } static HRESULT WINAPI hrSetEvent(HANDLE event) { BOOL ret; SetLastError(0); ret = SetEvent(event); if (ret == 0) return lastErrorToHRESULT(); return S_OK; } static HRESULT WINAPI hrWaitForSingleObject(HANDLE handle, DWORD timeout) { DWORD ret; SetLastError(0); ret = WaitForSingleObject(handle, timeout); if (ret == WAIT_FAILED) return lastErrorToHRESULT(); return S_OK; } struct testingTimer { LARGE_INTEGER start; LARGE_INTEGER end; }; testingTimer *testingNewTimer(void) { return testingprivNew(testingTimer); } void testingFreeTimer(testingTimer *t) { testingprivFree(t); } void testingTimerStart(testingTimer *t) { QueryPerformanceCounter(&(t->start)); } void testingTimerEnd(testingTimer *t) { QueryPerformanceCounter(&(t->end)); } int64_t testingTimerNsec(testingTimer *t) { LARGE_INTEGER qpf; int64_t qpnsQuot, qpnsRem; int64_t c; int64_t ret; QueryPerformanceFrequency(&qpf); qpnsQuot = testingNsecPerSec / qpf.QuadPart; qpnsRem = testingNsecPerSec % qpf.QuadPart; c = t->end.QuadPart - t->start.QuadPart; ret = c * qpnsQuot; ret += (c * qpnsRem) / qpf.QuadPart; return ret; } // note: the idea for the SetThreadContext() nuttery is from https://www.codeproject.com/Articles/71529/Exception-Injection-Throwing-an-Exception-in-Other static jmp_buf timeout_ret; static void onTimeout(void) { longjmp(timeout_ret, 1); } static HANDLE timeout_timer; static HANDLE timeout_finished; static HANDLE timeout_targetThread; static HRESULT timeout_hr; static void setContextForGet(CONTEXT *ctx) { ZeroMemory(ctx, sizeof (CONTEXT)); ctx->ContextFlags = CONTEXT_CONTROL; } static void setContextForSet(CONTEXT *ctx) { #if defined(_AMD64_) ctx->Rip = (DWORD64) onTimeout; #elif defined(_ARM_) ctx->Pc = (DWORD) onTimeout; #elif defined(_ARM64_) ctx->Pc = (DWORD64) onTimeout; #elif defined(_X86_) ctx->Eip = (DWORD) onTimeout; #elif defined(_IA64_) // TODO verify that this is correct ctx->StIIP = (ULONGLONG) onTimeout; #else #error unknown CPU architecture; cannot create CONTEXT objects for CPU-specific Windows test code #endif } static unsigned __stdcall timerThreadProc(void *data) { HANDLE objects[2]; CONTEXT ctx; DWORD which; HRESULT hr; objects[0] = timeout_timer; objects[1] = timeout_finished; timeout_hr = hrWaitForMultipleObjectsEx(2, objects, FALSE, INFINITE, FALSE, &which); if (timeout_hr != S_OK) // act as if we timed out; the other thread will see the error which = WAIT_OBJECT_0; if (which == WAIT_OBJECT_0 + 1) // we succeeded; do nothing return 0; // we timed out (or there was an error); signal it hr = hrSuspendThread(timeout_targetThread); if (hr != S_OK) testingprivInternalError("error calling SuspendThread() after timeout: 0x%08I32X", hr); setContextForGet(&ctx); hr = hrGetThreadContext(timeout_targetThread, &ctx); if (hr != S_OK) testingprivInternalError("error calling GetThreadContext() after timeout: 0x%08I32X", hr); setContextForSet(&ctx); hr = hrSetThreadContext(timeout_targetThread, &ctx); if (hr != S_OK) testingprivInternalError("error calling SetThreadContext() after timeout: 0x%08I32X", hr); // and force the thread to return from GetMessage(), if we are indeed in that // TODO decide whether to check errors // TODO either way, check errors in GetThreadId() PostThreadMessage(GetThreadId(timeout_targetThread), WM_NULL, 0, 0); hr = hrResumeThread(timeout_targetThread); if (hr != S_OK) testingprivInternalError("error calling ResumeThread() after timeout: 0x%08I32X", hr); return 0; } void testingprivRunWithTimeout(testingT *t, const char *file, long line, int64_t timeout, void (*f)(testingT *t, void *data), void *data, const char *comment, int failNowOnError) { char *timeoutstr; int closeTargetThread = 0; uintptr_t timerThread = 0; LARGE_INTEGER timer; int waitForTimerThread = 0; HRESULT hr; timeoutstr = testingNsecString(timeout); hr = hrDuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &timeout_targetThread, 0, FALSE, DUPLICATE_SAME_ACCESS); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error getting current thread for %s timeout: 0x%08I32X", comment, hr); testingTFail(t); goto out; } closeTargetThread = 1; hr = hrCreateWaitableTimerW(NULL, TRUE, NULL, &timeout_timer); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error creating timer for %s timeout: 0x%08I32X", comment, hr); testingTFail(t); goto out; } hr = hrCreateEventW(NULL, TRUE, FALSE, NULL, &timeout_finished); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error creating finished event for %s timeout: 0x%08I32X", comment, hr); testingTFail(t); goto out; } timer.QuadPart = timeout / 100; timer.QuadPart = -timer.QuadPart; hr = hrSetWaitableTimer(timeout_timer, &timer, 0, NULL, NULL, FALSE); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error applying %s timeout: 0x%08I32X", comment, hr); testingTFail(t); goto out; } // don't start the thread until after we call setjmp() hr = hr_beginthreadex(NULL, 0, timerThreadProc, NULL, CREATE_SUSPENDED, NULL, &timerThread); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error creating timer thread for %s timeout: 0x%08I32X", comment, hr); testingTFail(t); goto out; } waitForTimerThread = 1; if (setjmp(timeout_ret) == 0) { hr = hrResumeThread((HANDLE) timerThread); if (hr != S_OK) { testingprivTLogfFull(t, file, line, "error calling ResumeThread() to start timeout thread: 0x%08I32X", hr); testingTFail(t); waitForTimerThread = 0; goto out; } (*f)(t, data); failNowOnError = 0; // we succeeded } else if (timeout_hr == S_OK) { testingprivTLogfFull(t, file, line, "%s timeout passed (%s)", comment, timeoutstr); testingTFail(t); } else { testingprivTLogfFull(t, file, line, "error running timer thread for %s timeout: 0x%08I32X", comment, timeout_hr); testingTFail(t); } out: if (timerThread != 0) { if (waitForTimerThread) { hr = hrSetEvent(timeout_finished); if (hr != S_OK) testingprivInternalError("error signaling timer thread to finish for %s timeout: 0x%08I32X (this is fatal because that thread may interrupt us)", comment, hr); hr = hrWaitForSingleObject((HANDLE) timerThread, INFINITE); if (hr != S_OK) testingprivInternalError("error waiting for timer thread to quit for %s timeout: 0x%08I32X (this is fatal because that thread may interrupt us)", comment, hr); } CloseHandle((HANDLE) timerThread); } if (timeout_finished != NULL) { CloseHandle(timeout_finished); timeout_finished = NULL; } if (timeout_timer != NULL) { CloseHandle(timeout_timer); timeout_timer = NULL; } if (closeTargetThread) CloseHandle(timeout_targetThread); timeout_targetThread = NULL; testingFreeNsecString(timeoutstr); if (failNowOnError) testingTFailNow(t); } void testingSleep(int64_t nsec) { HANDLE timer; LARGE_INTEGER duration; HRESULT hr; // TODO rename all the other durations that are timeout or timer to duration or nsec, both here and in the Unix/Darwin code duration.QuadPart = nsec / 100; duration.QuadPart = -duration.QuadPart; hr = hrCreateWaitableTimerW(NULL, TRUE, NULL, &timer); if (hr != S_OK) goto fallback; hr = hrSetWaitableTimer(timer, &duration, 0, NULL, NULL, FALSE); if (hr != S_OK) { CloseHandle(timer); goto fallback; } hr = hrWaitForSingleObject(timer, INFINITE); CloseHandle(timer); if (hr == S_OK) return; fallback: // this has lower resolution, but we can't detect a failure, so use it as a fallback Sleep((DWORD) (nsec / testingNsecPerMsec)); } struct testingThread { uintptr_t handle; void (*f)(void *data); void *data; }; static unsigned __stdcall threadThreadProc(void *data) { testingThread *t = (testingThread *) data; (*(t->f))(t->data); return 0; } // TODO instead of panicking, should these functions report errors to a testingT? testingThread *testingNewThread(void (*f)(void *data), void *data) { testingThread *t; HRESULT hr; t = testingprivNew(testingThread); t->f = f; t->data = data; hr = hr_beginthreadex(NULL, 0, threadThreadProc, t, 0, NULL, &(t->handle)); if (hr != S_OK) testingprivInternalError("error creating thread: 0x%08I32X", hr); return t; } void testingThreadWaitAndFree(testingThread *t) { HRESULT hr; hr = hrWaitForSingleObject((HANDLE) (t->handle), INFINITE); if (hr != S_OK) testingprivInternalError("error waiting for thread to finish: 0x%08I32X", hr); CloseHandle((HANDLE) (t->handle)); testingprivFree(t); }