From f2a66788f8fae03f0c7cdec943081ee4a2745bb7 Mon Sep 17 00:00:00 2001 From: Colin Finck Date: Mon, 8 Jun 2015 17:15:44 +0000 Subject: [PATCH] [LOCALSPL_APITEST] Write an API-Test for localspl.dll. As the original localspl.dll from Windows Server 2003 relies on proper initialization inside spoolsv.exe, we cannot test it standalone as usual. To make testing possible anyway, this program basically does four things: - Injecting our testing code into spoolsv.exe. - Registering and running us as a service in the SYSTEM security context like spoolsv.exe, so that injection is possible at all. - Sending the test name and receiving the console output over named pipes. - Redirecting the received console output to stdout again, so it looks and feels like a standard API-Test. Nevertheless, the testing code in fpEnumPrinters.c is still written like a usual test. The known ok(), skip(), etc. macros can be used as usual, their output is just redirected through the named pipes. Thanks to Thomas for giving me the tip about injecting code into spoolsv! :) svn path=/branches/colins-printing-for-freedom/; revision=68080 --- rostests/apitests/CMakeLists.txt | 1 + rostests/apitests/localspl/CMakeLists.txt | 13 ++ rostests/apitests/localspl/dll/CMakeLists.txt | 11 ++ .../apitests/localspl/dll/fpEnumPrinters.c | 72 +++++++ rostests/apitests/localspl/dll/main.c | 89 +++++++++ rostests/apitests/localspl/localspl_apitest.h | 17 ++ rostests/apitests/localspl/service.c | 182 ++++++++++++++++++ rostests/apitests/localspl/testlist.c | 22 +++ rostests/apitests/localspl/tests.c | 175 +++++++++++++++++ 9 files changed, 582 insertions(+) create mode 100644 rostests/apitests/localspl/CMakeLists.txt create mode 100644 rostests/apitests/localspl/dll/CMakeLists.txt create mode 100644 rostests/apitests/localspl/dll/fpEnumPrinters.c create mode 100644 rostests/apitests/localspl/dll/main.c create mode 100644 rostests/apitests/localspl/localspl_apitest.h create mode 100644 rostests/apitests/localspl/service.c create mode 100644 rostests/apitests/localspl/testlist.c create mode 100644 rostests/apitests/localspl/tests.c diff --git a/rostests/apitests/CMakeLists.txt b/rostests/apitests/CMakeLists.txt index f608c27b14e..b964e117ef5 100644 --- a/rostests/apitests/CMakeLists.txt +++ b/rostests/apitests/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(gdi32) if(NOT ARCH STREQUAL "amd64") add_subdirectory(kernel32) endif() +add_subdirectory(localspl) add_subdirectory(msvcrt) add_subdirectory(ntdll) add_subdirectory(powrprof) diff --git a/rostests/apitests/localspl/CMakeLists.txt b/rostests/apitests/localspl/CMakeLists.txt new file mode 100644 index 00000000000..e6c26f69282 --- /dev/null +++ b/rostests/apitests/localspl/CMakeLists.txt @@ -0,0 +1,13 @@ + +add_subdirectory(dll) + +list(APPEND SOURCE + service.c + tests.c + testlist.c) + +add_executable(localspl_apitest ${SOURCE}) +target_link_libraries(localspl_apitest wine ${PSEH_LIB}) +set_module_type(localspl_apitest win32cui) +add_importlibs(localspl_apitest advapi32 winspool msvcrt kernel32 ntdll) +add_cd_file(TARGET localspl_apitest DESTINATION reactos/bin FOR all) diff --git a/rostests/apitests/localspl/dll/CMakeLists.txt b/rostests/apitests/localspl/dll/CMakeLists.txt new file mode 100644 index 00000000000..08b50d8d8e3 --- /dev/null +++ b/rostests/apitests/localspl/dll/CMakeLists.txt @@ -0,0 +1,11 @@ + +list(APPEND SOURCE + fpEnumPrinters.c + main.c) + +add_library(localspl_apitest.dll SHARED ${SOURCE}) +target_link_libraries(localspl_apitest.dll wine ${PSEH_LIB}) +set_module_type(localspl_apitest.dll win32dll) +add_importlibs(localspl_apitest.dll msvcrt kernel32 ntdll) +set_target_properties(localspl_apitest.dll PROPERTIES SUFFIX "") +add_cd_file(TARGET localspl_apitest.dll DESTINATION reactos/bin FOR all) diff --git a/rostests/apitests/localspl/dll/fpEnumPrinters.c b/rostests/apitests/localspl/dll/fpEnumPrinters.c new file mode 100644 index 00000000000..657dbd7a85e --- /dev/null +++ b/rostests/apitests/localspl/dll/fpEnumPrinters.c @@ -0,0 +1,72 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests Injected DLL + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Tests for fpEnumPrinters + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +#include + +#define WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include + +#include "../localspl_apitest.h" + +START_TEST(fpEnumPrinters) +{ + DWORD cbNeeded; + DWORD dwReturned; + HMODULE hLocalspl; + PInitializePrintProvidor pfnInitializePrintProvidor; + PRINTPROVIDOR pp; + PPRINTER_INFO_1W pPrinterInfo1; + + // Get us a handle to the loaded localspl.dll. + hLocalspl = GetModuleHandleW(L"localspl"); + if (!hLocalspl) + { + skip("GetModuleHandleW failed with error %lu!\n", GetLastError()); + return; + } + + // Get a pointer to its InitializePrintProvidor function. + pfnInitializePrintProvidor = (PInitializePrintProvidor)GetProcAddress(hLocalspl, "InitializePrintProvidor"); + if (!pfnInitializePrintProvidor) + { + skip("GetProcAddress failed with error %lu!\n", GetLastError()); + return; + } + + // Get localspl's function pointers. + if (!pfnInitializePrintProvidor(&pp, sizeof(pp), NULL)) + { + skip("pfnInitializePrintProvidor failed with error %lu!\n", GetLastError()); + return; + } + + // Verify that localspl only returns information about a single print provider (namely itself). + cbNeeded = 0xDEADBEEF; + dwReturned = 0xDEADBEEF; + SetLastError(0xDEADBEEF); + ok(!pp.fpEnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_NAME, NULL, 1, NULL, 0, &cbNeeded, &dwReturned), "fpEnumPrinters returns TRUE\n"); + ok(GetLastError() == ERROR_INSUFFICIENT_BUFFER, "fpEnumPrinters returns error %lu!\n", GetLastError()); + ok(cbNeeded > 0, "cbNeeded is 0!\n"); + ok(dwReturned == 0, "dwReturned is %lu!\n", dwReturned); + + SetLastError(0xDEADBEEF); + pPrinterInfo1 = HeapAlloc(GetProcessHeap(), 0, cbNeeded); + ok(pp.fpEnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_NAME, NULL, 1, (PBYTE)pPrinterInfo1, cbNeeded, &cbNeeded, &dwReturned), "fpEnumPrinters returns FALSE\n"); + ok(GetLastError() == ERROR_SUCCESS, "fpEnumPrinters returns error %lu!\n", GetLastError()); + ok(cbNeeded > 0, "cbNeeded is 0!\n"); + ok(dwReturned == 1, "dwReturned is %lu!\n", dwReturned); + + // Verify the actual strings returned. + ok(wcscmp(pPrinterInfo1->pName, L"Windows NT Local Print Providor") == 0, "pPrinterInfo1->pName is \"%S\"!\n", pPrinterInfo1->pName); + ok(wcscmp(pPrinterInfo1->pDescription, L"Windows NT Local Printers") == 0, "pPrinterInfo1->pDescription is \"%S\"!\n", pPrinterInfo1->pDescription); + ok(wcscmp(pPrinterInfo1->pComment, L"Locally connected Printers") == 0, "pPrinterInfo1->pComment is \"%S\"!\n", pPrinterInfo1->pComment); +} diff --git a/rostests/apitests/localspl/dll/main.c b/rostests/apitests/localspl/dll/main.c new file mode 100644 index 00000000000..4a558e592bb --- /dev/null +++ b/rostests/apitests/localspl/dll/main.c @@ -0,0 +1,89 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests Injected DLL + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Main functions + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +#define __ROS_LONG64__ + +#define STANDALONE +#include + +#define WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include + + +#include "../localspl_apitest.h" + +//#define NDEBUG +#include + +// Test list +extern void func_fpEnumPrinters(void); + +const struct test winetest_testlist[] = +{ + { "fpEnumPrinters", func_fpEnumPrinters }, + + { 0, 0 } +}; + +// Running the tests from the injected DLL and redirecting their output to the pipe. +BOOL WINAPI +DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) +{ + char szTestName[150]; + DWORD cbRead; + FILE* fpStdout; + HANDLE hCommandPipe; + int iOldStdout; + + // We only want to run our test once when the DLL is injected to the process. + if (fdwReason != DLL_PROCESS_ATTACH) + return TRUE; + + // Read the test to run from the command pipe. + hCommandPipe = CreateFileW(COMMAND_PIPE_NAME, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); + if (hCommandPipe == INVALID_HANDLE_VALUE) + { + DPRINT("DLL: CreateFileW failed for the command pipe with error %lu!\n", GetLastError()); + return FALSE; + } + + if (!ReadFile(hCommandPipe, szTestName, sizeof(szTestName), &cbRead, NULL)) + { + DPRINT("DLL: ReadFile failed for the command pipe with error %lu!\n", GetLastError()); + return FALSE; + } + + CloseHandle(hCommandPipe); + + // Check if the test name is valid. + if (!find_test(szTestName)) + { + DPRINT("DLL: Got invalid test name \"%s\"!\n", szTestName); + return FALSE; + } + + // Backup our current stdout and set it to the output pipe. + iOldStdout = _dup(_fileno(stdout)); + fpStdout = _wfreopen(OUTPUT_PIPE_NAME, L"w", stdout); + setbuf(stdout, NULL); + + // Run the test. + run_test(szTestName); + + // Restore stdout to the previous value. + fclose(fpStdout); + _dup2(iOldStdout, _fileno(stdout)); + + // Return FALSE so that our DLL is immediately unloaded. + return FALSE; +} diff --git a/rostests/apitests/localspl/localspl_apitest.h b/rostests/apitests/localspl/localspl_apitest.h new file mode 100644 index 00000000000..402a941d0ca --- /dev/null +++ b/rostests/apitests/localspl/localspl_apitest.h @@ -0,0 +1,17 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Shared definitions for the test program and the test DLL + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +#ifndef _LOCALSPL_APITEST_H +#define _LOCALSPL_APITEST_H + +#define COMMAND_PIPE_NAME L"\\\\.\\pipe\\localspl_apitest_command_pipe" +#define OUTPUT_PIPE_NAME L"\\\\.\\pipe\\localspl_apitest_output_pipe" +#define SERVICE_NAME L"localspl_apitest_service" + +typedef BOOL (WINAPI *PInitializePrintProvidor)(LPPRINTPROVIDOR, DWORD, LPWSTR); + +#endif \ No newline at end of file diff --git a/rostests/apitests/localspl/service.c b/rostests/apitests/localspl/service.c new file mode 100644 index 00000000000..384b0759d02 --- /dev/null +++ b/rostests/apitests/localspl/service.c @@ -0,0 +1,182 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Functions needed to run our code as a service. This is needed to run in SYSTEM security context. + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +#include + +#define WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include + +#include "localspl_apitest.h" + +//#define NDEBUG +#include + + +static void +_DoDLLInjection() +{ + DWORD cbDLLPath; + HANDLE hProcess; + HANDLE hSnapshot; + HANDLE hThread; + PROCESSENTRY32W pe; + PVOID pLoadLibraryAddress; + PVOID pLoadLibraryArgument; + PWSTR p; + WCHAR wszFilePath[MAX_PATH]; + + // Get the full path to our EXE file. + if (!GetModuleFileNameW(NULL, wszFilePath, _countof(wszFilePath))) + { + DPRINT("GetModuleFileNameW failed with error %lu!\n", GetLastError()); + return; + } + + // Replace the extension. + p = wcsrchr(wszFilePath, L'.'); + if (!p) + { + DPRINT("File path has no file extension: %S\n", wszFilePath); + return; + } + + wcscpy(p, L".dll"); + cbDLLPath = (wcslen(wszFilePath) + 1) * sizeof(WCHAR); + + // Create a snapshot of the currently running processes. + hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) + { + DPRINT("CreateToolhelp32Snapshot failed with error %lu!\n", GetLastError()); + return; + } + + // Enumerate through all running processes. + pe.dwSize = sizeof(pe); + if (!Process32FirstW(hSnapshot, &pe)) + { + DPRINT("Process32FirstW failed with error %lu!\n", GetLastError()); + return; + } + + do + { + // Check if this is the spooler server process. + if (wcsicmp(pe.szExeFile, L"spoolsv.exe") != 0) + continue; + + // Open a handle to the process. + hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID); + if (!hProcess) + { + DPRINT("OpenProcess failed with error %lu!\n", GetLastError()); + return; + } + + // Get the address of LoadLibraryW. + pLoadLibraryAddress = (PVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"); + if (!pLoadLibraryAddress) + { + DPRINT("GetProcAddress failed with error %lu!\n", GetLastError()); + return; + } + + // Allocate memory for the DLL path in the spooler process. + pLoadLibraryArgument = VirtualAllocEx(hProcess, NULL, cbDLLPath, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!pLoadLibraryArgument) + { + DPRINT("VirtualAllocEx failed with error %lu!\n", GetLastError()); + return; + } + + // Write the DLL path to the process memory. + if (!WriteProcessMemory(hProcess, pLoadLibraryArgument, wszFilePath, cbDLLPath, NULL)) + { + DPRINT("WriteProcessMemory failed with error %lu!\n", GetLastError()); + return; + } + + // Create a new thread in the spooler process that calls LoadLibraryW as the start routine with our DLL as the argument. + // This effectively injects our DLL into the spooler process and we can inspect localspl.dll there just like the spooler. + hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryAddress, pLoadLibraryArgument, 0, NULL); + if (!hThread) + { + DPRINT("CreateRemoteThread failed with error %lu!\n", GetLastError()); + return; + } + + CloseHandle(hThread); + break; + } + while (Process32NextW(hSnapshot, &pe)); +} + +static DWORD WINAPI +_ServiceControlHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) +{ + return NO_ERROR; +} + +static void WINAPI +_ServiceMain(DWORD dwArgc, LPWSTR* lpszArgv) +{ + SERVICE_STATUS_HANDLE hServiceStatus; + SERVICE_STATUS ServiceStatus; + + UNREFERENCED_PARAMETER(dwArgc); + UNREFERENCED_PARAMETER(lpszArgv); + + // Register our service for control. + hServiceStatus = RegisterServiceCtrlHandlerExW(SERVICE_NAME, _ServiceControlHandlerEx, NULL); + + // Report SERVICE_RUNNING status. + ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; + ServiceStatus.dwServiceSpecificExitCode = 0; + ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + ServiceStatus.dwWaitHint = 4000; + ServiceStatus.dwWin32ExitCode = NO_ERROR; + ServiceStatus.dwCurrentState = SERVICE_RUNNING; + SetServiceStatus(hServiceStatus, &ServiceStatus); + + // Do our funky crazy stuff. + _DoDLLInjection(); + + // Our work is done. + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(hServiceStatus, &ServiceStatus); +} + +START_TEST(service) +{ + int argc; + char** argv; + + SERVICE_TABLE_ENTRYW ServiceTable[] = + { + { SERVICE_NAME, _ServiceMain }, + { NULL, NULL } + }; + + // This is no real test, but an easy way to integrate the service handler routines into the API-Test executable. + // Therefore, bail out if someone tries to run "service" as a usual test. + argc = winetest_get_mainargs(&argv); + if (argc != 3) + return; + + // If we have exactly 3 arguments, we're run as a service, so initialize the corresponding service handler functions. + StartServiceCtrlDispatcherW(ServiceTable); + + // Prevent the testing framework from outputting a "0 tests executed" line here. + ExitProcess(0); +} diff --git a/rostests/apitests/localspl/testlist.c b/rostests/apitests/localspl/testlist.c new file mode 100644 index 00000000000..7102484b8f1 --- /dev/null +++ b/rostests/apitests/localspl/testlist.c @@ -0,0 +1,22 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Test list + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +#define __ROS_LONG64__ + +#define STANDALONE +#include + +extern void func_fpEnumPrinters(void); +extern void func_service(void); + +const struct test winetest_testlist[] = +{ + { "fpEnumPrinters", func_fpEnumPrinters }, + { "service", func_service }, + + { 0, 0 } +}; diff --git a/rostests/apitests/localspl/tests.c b/rostests/apitests/localspl/tests.c new file mode 100644 index 00000000000..d733ea0a15c --- /dev/null +++ b/rostests/apitests/localspl/tests.c @@ -0,0 +1,175 @@ +/* + * PROJECT: ReactOS Local Spooler API Tests + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Test list + * COPYRIGHT: Copyright 2015 Colin Finck + */ + +/* + * The original localspl.dll from Windows Server 2003 is not easily testable. + * It relies on a proper initialization inside spoolsv.exe, so we can't just load it in an API-Test as usual. + * See https://www.reactos.org/pipermail/ros-dev/2015-June/017395.html for more information. + * + * To make testing possible anyway, this program basically does four things: + * - Injecting our testing code into spoolsv.exe. + * - Registering and running us as a service in the SYSTEM security context like spoolsv.exe, so that injection is possible at all. + * - Sending the test name and receiving the console output over named pipes. + * - Redirecting the received console output to stdout again, so it looks and feels like a standard API-Test. + * + * To simplify debugging of the injected code, it is entirely separated into a DLL file localspl_apitest.dll. + * What we actually inject is a LoadLibraryW call, so that the DLL is loaded gracefully without any hacks. + * Therefore, you can just attach your debugger to the spoolsv.exe process and set breakpoints on the localspl_apitest.dll code. + */ + +#include + +#define WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "localspl_apitest.h" + + +static void +_RunRemoteTest(const char* szTestName) +{ + char szBuffer[1024]; + DWORD cbRead; + DWORD cbWritten; + HANDLE hCommandPipe; + HANDLE hOutputPipe; + PWSTR p; + SC_HANDLE hSC; + SC_HANDLE hService; + WCHAR wszFilePath[MAX_PATH + 20]; + WIN32_FIND_DATAW fd; + + // Do a dummy EnumPrintersW call. + // This guarantees that the Spooler Service has actually loaded localspl.dll, which is a requirement for our injected DLL to work properly. + EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_NAME, NULL, 1, NULL, 0, &cbRead, &cbWritten); + + // Get the full path to our EXE file. + if (!GetModuleFileNameW(NULL, wszFilePath, MAX_PATH)) + { + skip("GetModuleFileNameW failed with error %lu!\n", GetLastError()); + return; + } + + // Replace the extension. + p = wcsrchr(wszFilePath, L'.'); + if (!p) + { + skip("File path has no file extension: %S\n", wszFilePath); + return; + } + + wcscpy(p, L".dll"); + + // Check if the corresponding DLL file exists. + if (!FindFirstFileW(wszFilePath, &fd)) + { + skip("My DLL file \"%S\" does not exist!\n", wszFilePath); + return; + } + + // Change the extension back to .exe and add the parameters. + wcscpy(p, L".exe service dummy"); + + // Open a handle to the service manager. + hSC = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS); + if (!hSC) + { + skip("OpenSCManagerW failed with error %lu!\n", GetLastError()); + return; + } + + // Try to open the service if we've created it in a previous run. + hService = OpenServiceW(hSC, SERVICE_NAME, SERVICE_ALL_ACCESS); + if (!hService) + { + if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) + { + // Create the service. + hService = CreateServiceW(hSC, SERVICE_NAME, NULL, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, wszFilePath, NULL, NULL, NULL, NULL, NULL); + if (!hService) + { + skip("CreateServiceW failed with error %lu!\n", GetLastError()); + return; + } + } + else + { + skip("OpenServiceW failed with error %lu!\n", GetLastError()); + return; + } + } + + // Create pipes for the communication with the injected DLL. + hCommandPipe = CreateNamedPipeW(COMMAND_PIPE_NAME, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 1024, 1024, 10000, NULL); + if (hCommandPipe == INVALID_HANDLE_VALUE) + { + skip("CreateNamedPipeW failed for the command pipe with error %lu!\n", GetLastError()); + return; + } + + hOutputPipe = CreateNamedPipeW(OUTPUT_PIPE_NAME, PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 1024, 1024, 10000, NULL); + if (hOutputPipe == INVALID_HANDLE_VALUE) + { + skip("CreateNamedPipeW failed for the output pipe with error %lu!\n", GetLastError()); + return; + } + + // Start the service with "service" and a dummy parameter (to distinguish it from a call by rosautotest to localspl_apitest:service) + if (!StartServiceW(hService, 0, NULL)) + { + skip("StartServiceW failed with error %lu!\n", GetLastError()); + return; + } + + CloseServiceHandle(hService); + CloseServiceHandle(hSC); + + // Wait till it has injected the DLL and the DLL expects its test name. + if (!ConnectNamedPipe(hCommandPipe, NULL) && GetLastError() != ERROR_PIPE_CONNECTED) + { + skip("ConnectNamedPipe failed for the command pipe with error %lu!\n", GetLastError()); + return; + } + + // Send the test name. + if (!WriteFile(hCommandPipe, szTestName, strlen(szTestName) + sizeof(char), &cbWritten, NULL)) + { + skip("WriteFile failed with error %lu!\n", GetLastError()); + return; + } + + CloseHandle(hCommandPipe); + + // Now wait for the DLL to connect to the output pipe. + if (!ConnectNamedPipe(hOutputPipe, NULL)) + { + skip("ConnectNamedPipe failed for the output pipe with error %lu!\n", GetLastError()); + return; + } + + // Get all testing messages from the pipe and output them on stdout. + while (ReadFile(hOutputPipe, szBuffer, sizeof(szBuffer), &cbRead, NULL) && cbRead) + fwrite(szBuffer, sizeof(char), cbRead, stdout); + + CloseHandle(hOutputPipe); + + // Prevent the testing framework from outputting a "0 tests executed" line here. + ExitProcess(0); +} + +START_TEST(fpEnumPrinters) +{ + _RunRemoteTest("fpEnumPrinters"); +} -- 2.17.1