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"); +}