From 8ba4ef28755092ca9b34e69e0d067aa68fd9a783 Mon Sep 17 00:00:00 2001 From: Mikolaj Zalewski Date: Mon, 24 Sep 2007 11:14:52 -0700 Subject: [PATCH] services.exe: start a local RPC server --- .gitignore | 4 + dlls/advapi32/Makefile.in | 3 + dlls/advapi32/service.c | 154 +++++++++++++++++++++++--- dlls/advapi32/svcctl.idl | 1 dlls/advapi32/tests/service.c | 3 - include/wine/svcctl.idl | 68 ++++++++++++ programs/services/Makefile.in | 9 +- programs/services/rpc.c | 240 +++++++++++++++++++++++++++++++++++++++++ programs/services/services.c | 7 + programs/services/services.h | 4 + programs/services/svcctl.idl | 1 11 files changed, 468 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index f444e0c..9f7c636 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ dlls/advapi32/libadvapi32.def dlls/advapi32/tests/*.ok dlls/advapi32/tests/advapi32_crosstest.exe dlls/advapi32/tests/testlist.c +dlls/advapi32/svcctl.h +dlls/advapi32/svcctl_c.c dlls/advapi32/version.res dlls/advpack/libadvpack.def dlls/advpack/tests/*.ok @@ -641,6 +643,8 @@ programs/rpcss/irot_s.c programs/rpcss/rpcss programs/rundll32/rundll32 programs/services/services +programs/services/svcctl.h +programs/services/svcctl_s.c programs/spoolsv/spoolsv programs/start/rsrc.res programs/start/start diff --git a/dlls/advapi32/Makefile.in b/dlls/advapi32/Makefile.in index 8f6fe33..788c695 100644 --- a/dlls/advapi32/Makefile.in +++ b/dlls/advapi32/Makefile.in @@ -6,6 +6,7 @@ VPATH = @srcdir@ MODULE = advapi32.dll IMPORTLIB = libadvapi32.$(IMPLIBEXT) IMPORTS = kernel32 ntdll +DELAYIMPORTS = rpcrt4 C_SRCS = \ advapi.c \ @@ -22,6 +23,8 @@ C_SRCS = \ security.c \ service.c +IDL_C_SRCS = svcctl.idl + RC_SRCS = version.rc @MAKE_DLL_RULES@ diff --git a/dlls/advapi32/service.c b/dlls/advapi32/service.c index ce031ab..b2e9c25 100644 --- a/dlls/advapi32/service.c +++ b/dlls/advapi32/service.c @@ -37,8 +37,20 @@ #include "lmcons.h" #include "lmserver.h" #include "wine/list.h" +#include "svcctl.h" + WINE_DEFAULT_DEBUG_CHANNEL(advapi); +void __RPC_FAR * __RPC_USER MIDL_user_allocate(size_t len) +{ + return HeapAlloc(GetProcessHeap(), 0, len); +} + +void __RPC_USER MIDL_user_free(void __RPC_FAR * ptr) +{ + HeapFree(GetProcessHeap(), 0, ptr); +} + static const WCHAR szServiceManagerKey[] = { 'S','y','s','t','e','m','\\', 'C','u','r','r','e','n','t','C','o','n','t','r','o','l','S','e','t','\\', 'S','e','r','v','i','c','e','s',0 }; @@ -120,6 +132,8 @@ struct sc_handle SC_HANDLE_TYPE htype; DWORD ref_count; sc_handle_destructor destroy; + SvcCtlRpcHandle rpc_handle; /* RPC connection handle */ + POLICY_HANDLE server_handle; /* server-side handle */ }; struct sc_manager /* service control manager handle */ @@ -180,6 +194,8 @@ static void sc_handle_destroy_manager(st struct sc_manager *mgr = (struct sc_manager*) handle; TRACE("destroying SC Manager %p\n", mgr); + if (mgr->hdr.rpc_handle) + RpcBindingFree(&mgr->hdr.rpc_handle); if (mgr->hkey) RegCloseKey(mgr->hkey); } @@ -234,6 +250,95 @@ static inline LPWSTR SERV_dupmulti(LPCST } /****************************************************************************** + * RPC connection with servies.exe + */ + +BOOL check_services_exe() +{ + HANDLE hEvent = OpenEventW(SYNCHRONIZE, FALSE, SVCCTL_STARTED_EVENT); + if (hEvent == NULL) + { + static const WCHAR services[] = {'\\','s','e','r','v','i','c','e','s','.','e','x','e',0}; + PROCESS_INFORMATION out; + STARTUPINFOW si; + HANDLE wait_handles[2]; + WCHAR path[MAX_PATH]; + + if (!GetSystemDirectoryW(path, MAX_PATH - strlenW(services))) + return FALSE; + strcatW(path, services); + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + if (!CreateProcessW(path, path, NULL, NULL, FALSE, 0, NULL, NULL, &si, &out)) + { + ERR("Couldn't start services.exe: error %u\n", GetLastError()); + return FALSE; + } + CloseHandle(out.hThread); + + hEvent = CreateEventW(NULL, TRUE, FALSE, SVCCTL_STARTED_EVENT); + wait_handles[0] = hEvent; + wait_handles[1] = out.hProcess; + /* wait for the event to become available or the process to exit */ + if (WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE) == WAIT_OBJECT_0 + 1) + { + DWORD exit_code; + GetExitCodeProcess(out.hProcess, &exit_code); + CloseHandle(out.hProcess); + ERR("Unexpected termination of services.exe - exit code %d\n", exit_code); + return FALSE; + } + CloseHandle(out.hProcess); + } + + TRACE("Waiting for services.exe to be available\n"); + WaitForSingleObject(hEvent, INFINITE); + TRACE("Services.exe are available\n"); + CloseHandle(hEvent); + + return TRUE; +} + +SvcCtlRpcHandle connect_to_server(LPCWSTR server) +{ + WCHAR transport[] = SVCCTL_TRANSPORT; + WCHAR endpoint[] = SVCCTL_ENDPOINT; + LPWSTR server_copy = NULL; + RPC_WSTR binding_str; + RPC_STATUS status; + SvcCtlRpcHandle rpc_handle; + + /* unlike Windows we start services.exe on demand. We start it always as + * checking if this is our address can be tricky */ + if (!check_services_exe()) + return NULL; + + if (server) /* parameters of RpcStringBindingComposeW are not const */ + { + server_copy = HeapAlloc(GetProcessHeap(), 0, sizeof(WCHAR)*(strlenW(server) + 1)); + strcpyW(server_copy, server); + } + status = RpcStringBindingComposeW(NULL, transport, server_copy, endpoint, NULL, &binding_str); + HeapFree(GetProcessHeap(), 0, server_copy); + if (status != RPC_S_OK) + { + ERR("RpcStringBindingComposeW failed (%d)\n", (DWORD)status); + return NULL; + } + + status = RpcBindingFromStringBindingW(binding_str, &rpc_handle); + RpcStringFreeW(&binding_str); + + if (status != RPC_S_OK) + { + ERR("Couldn't connect to services.exe: error code %u\n", (DWORD)status); + return NULL; + } + + return rpc_handle; +} + +/****************************************************************************** * registry access functions and data */ static const WCHAR szDisplayName[] = { @@ -1029,28 +1134,21 @@ SC_HANDLE WINAPI OpenSCManagerW( LPCWSTR TRACE("(%s,%s,0x%08x)\n", debugstr_w(lpMachineName), debugstr_w(lpDatabaseName), dwDesiredAccess); - if( lpDatabaseName && lpDatabaseName[0] ) - { - if( strcmpiW( lpDatabaseName, SERVICES_ACTIVE_DATABASEW ) == 0 ) - { - /* noop, all right */ - } - else if( strcmpiW( lpDatabaseName, SERVICES_FAILED_DATABASEW ) == 0 ) - { - SetLastError( ERROR_DATABASE_DOES_NOT_EXIST ); - return NULL; - } - else - { - SetLastError( ERROR_INVALID_NAME ); - return NULL; - } - } - manager = sc_handle_alloc( SC_HTYPE_MANAGER, sizeof (struct sc_manager), sc_handle_destroy_manager ); if (!manager) return NULL; + + manager->hdr.rpc_handle = connect_to_server(lpMachineName); + if (manager->hdr.rpc_handle == NULL) + { + r = GetLastError(); + goto error; + } + + r = svcctl_OpenSCManagerW(manager->hdr.rpc_handle, NULL, lpDatabaseName, dwDesiredAccess, &manager->hdr.server_handle); + if (r!=ERROR_SUCCESS) + goto error; r = RegConnectRegistryW(lpMachineName,HKEY_LOCAL_MACHINE,&hReg); if (r!=ERROR_SUCCESS) @@ -1160,10 +1258,28 @@ BOOL WINAPI ControlService( SC_HANDLE hS BOOL WINAPI CloseServiceHandle( SC_HANDLE hSCObject ) { + struct sc_handle *obj; + DWORD err; + TRACE("%p\n", hSCObject); + if (hSCObject == NULL) + { + SetLastError(ERROR_INVALID_HANDLE); + return FALSE; + } - sc_handle_free( (struct sc_handle*) hSCObject ); + obj = (struct sc_handle *)hSCObject; + if (obj->rpc_handle) /* service handles currently don't have RPC connections */ + err = svcctl_CloseServiceHandle(obj->rpc_handle, &obj->server_handle); + else + err = ERROR_SUCCESS; + sc_handle_free( obj ); + if (err != ERROR_SUCCESS) + { + SetLastError(err); + return FALSE; + } return TRUE; } diff --git a/dlls/advapi32/svcctl.idl b/dlls/advapi32/svcctl.idl new file mode 100644 index 0000000..101e0f2 --- /dev/null +++ b/dlls/advapi32/svcctl.idl @@ -0,0 +1 @@ +#include "wine/svcctl.idl" diff --git a/dlls/advapi32/tests/service.c b/dlls/advapi32/tests/service.c index 7aea906..7b93df8 100644 --- a/dlls/advapi32/tests/service.c +++ b/dlls/advapi32/tests/service.c @@ -699,11 +699,8 @@ static void test_close(void) /* NULL handle */ SetLastError(0xdeadbeef); ret = CloseServiceHandle(NULL); - todo_wine - { ok(!ret, "Expected failure\n"); ok(GetLastError() == ERROR_INVALID_HANDLE, "Expected ERROR_INVALID_HANDLE, got %d\n", GetLastError()); - } /* TODO: Add some tests with invalid handles. These produce errors on Windows but crash on Wine */ diff --git a/include/wine/svcctl.idl b/include/wine/svcctl.idl new file mode 100644 index 0000000..fefd12f --- /dev/null +++ b/include/wine/svcctl.idl @@ -0,0 +1,68 @@ +/* + * svcctl interface definitions - exported by services.exe to access the + * services database + * + * Copyright 2007 Google (Mikolaj Zalewski) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +import "wtypes.idl"; + +/* + * some defined for the C code + */ +cpp_quote("#define SVCCTL_TRANSPORT {'n','c','a','c','n','_','n','p',0}"); +cpp_quote("#define SVCCTL_ENDPOINT {'\\\\','p','i','p','e','\\\\','s','v','c','c','t','l',0}"); + +/* Not the Windows event name - if needed the true one can be found in Inside Windows */ +cpp_quote("#define SVCCTL_STARTED_EVENT (const WCHAR[]){'_','_','w','i','n','e','_','S','v','c','c','t','l','S','t','a','r','t','e','d',0}"); + + +/* Based on the Samba IDL. Some functions are compatible with Windows but some + * aren't and many are missing (thus function numbers are different) so we + * don't use the Windows uuid which is 367abb81-9844-35f1-ad32-98f038001003 . + */ +[ uuid(78e2d9e8-d2a8-40d8-9bbe-7328cc5a9c32), + version(2.0), + pointer_default(unique), + endpoint("ncacn_np:[\\pipe\\svcctl]"), + helpstring("Service Control") +] interface svcctl +{ + typedef handle_t SvcCtlRpcHandle; + + typedef struct _POLICY_HANDLE + { + DWORD dwHandleType; + GUID uuid; + } POLICY_HANDLE; + + /* Compatible with Windows function 0x00 */ + DWORD svcctl_CloseServiceHandle( + [in] SvcCtlRpcHandle rpc_handle, + [in,out] POLICY_HANDLE *handle + ); + + /* Compatible with Windows function 0x0f */ + DWORD svcctl_OpenSCManagerW( + SvcCtlRpcHandle rpc_handle, + [in,unique] LPCWSTR MachineName, + [in,unique] LPCWSTR DatabaseName, + [in] DWORD dwAccessMask, + [out] POLICY_HANDLE *handle + ); + +} diff --git a/programs/services/Makefile.in b/programs/services/Makefile.in index 0aa642c..bd05be8 100644 --- a/programs/services/Makefile.in +++ b/programs/services/Makefile.in @@ -4,10 +4,13 @@ SRCDIR = @srcdir@ VPATH = @srcdir@ MODULE = services.exe APPMODE = -mwindows -IMPORTS = advapi32 kernel32 +IMPORTS = rpcrt4 advapi32 kernel32 ntdll -C_SRCS = services.c \ - utils.c +C_SRCS = rpc.c \ + services.c \ + utils.c + +IDL_S_SRCS = svcctl.idl @MAKE_PROG_RULES@ diff --git a/programs/services/rpc.c b/programs/services/rpc.c new file mode 100644 index 0000000..9bb936f --- /dev/null +++ b/programs/services/rpc.c @@ -0,0 +1,240 @@ +/* + * Services.exe - RPC functions + * + * Copyright 2007 Google (Mikolaj Zalewski) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include +#include +#include + +#include "wine/list.h" +#include "wine/unicode.h" +#include "wine/debug.h" + +#include "services.h" +#include "svcctl.h" + +extern HANDLE __wine_make_process_system(void); + +WINE_DEFAULT_DEBUG_CHANNEL(services); + +static CRITICAL_SECTION g_handle_table_cs; +static CRITICAL_SECTION_DEBUG g_handle_table_cs_debug = +{ + 0, 0, &g_handle_table_cs, + { &g_handle_table_cs_debug.ProcessLocksList, + &g_handle_table_cs_debug.ProcessLocksList }, + 0, 0, { (DWORD_PTR)(__FILE__ ": g_handle_table_cs") } +}; +static CRITICAL_SECTION g_handle_table_cs = { &g_handle_table_cs_debug, -1, 0, 0, 0, 0 }; + +static const GENERIC_MAPPING g_scm_generic = { + (STANDARD_RIGHTS_READ | SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_QUERY_LOCK_STATUS), + (STANDARD_RIGHTS_WRITE | SC_MANAGER_CREATE_SERVICE | SC_MANAGER_MODIFY_BOOT_CONFIG), + (STANDARD_RIGHTS_EXECUTE | SC_MANAGER_CONNECT | SC_MANAGER_LOCK), + SC_MANAGER_ALL_ACCESS +}; + +typedef struct _handle_uuid /* we interpret the uuid field of the POLICY_HANDLE as such a structure */ +{ + ULONG index; + DWORD padding; + LONGLONG dwHandleKey; +} handle_uuid; + +RTL_HANDLE_TABLE g_handle_table; + +#define HANDLE_TYPE_DONT_CARE 0 +#define HANDLE_TYPE_MANAGER 1 +#define HANDLE_TYPE_SERVICE 2 + +typedef struct _handle_data +{ + ULONG_PTR ulHandleFlags; /* bit #0 used by handle table */ + DWORD dwType; + DWORD dwAccess; + LONGLONG dwHandleKey; +} handle_data; + +static void init_policy_handle(POLICY_HANDLE *handle, ULONG index, handle_data *data) +{ + handle_uuid *uuid; + + RtlGenRandom(&data->dwHandleKey, sizeof(data->dwHandleKey)); + + handle->dwHandleType = 0; + uuid = (handle_uuid *)&handle->uuid; + uuid->index = index; + uuid->padding = 0; + uuid->dwHandleKey = data->dwHandleKey; +} + +/* Check if the given handle is valid, of a good type and allows the requested access. Note that as this + * function returns on success a pointer to handle data so in such a case the handle table lock is held. + */ +static DWORD validate_policy_handle(POLICY_HANDLE *handle, DWORD dwType, DWORD dwNeededAccess, handle_data **outData) +{ + handle_data *data; + handle_uuid *uuid; + /* FIXME: should we check if the handle is created by the current session? */ + if (handle->dwHandleType != 0) + { + WINE_ERR("Invalid handle passed - invalid dwType\n"); + return ERROR_INVALID_HANDLE; + } + + uuid = (handle_uuid *)&handle->uuid; + EnterCriticalSection(&g_handle_table_cs); + if (!RtlIsValidIndexHandle(&g_handle_table, uuid->index, (RTL_HANDLE **)&data)) + { + WINE_ERR("Invalid handle passed - invalid index %d\n", uuid->index); + LeaveCriticalSection(&g_handle_table_cs); + return ERROR_INVALID_HANDLE; + } + + if (uuid->dwHandleKey != data->dwHandleKey || uuid->padding != 0) + { + WINE_ERR("Invalid handle passed - invalid key\n"); + LeaveCriticalSection(&g_handle_table_cs); + return ERROR_INVALID_HANDLE; + } + + if (dwType != HANDLE_TYPE_DONT_CARE && data->dwType != dwType) + { + WINE_ERR("Handle is of an invalid type\n"); + LeaveCriticalSection(&g_handle_table_cs); + return ERROR_INVALID_HANDLE; + } + + if ((dwNeededAccess & data->dwAccess) != dwNeededAccess) + { + WINE_ERR("Access denied - handle created with access %x, needed %x\n", data->dwAccess, dwNeededAccess); + LeaveCriticalSection(&g_handle_table_cs); + return ERROR_ACCESS_DENIED; + } + + *outData = data; + return ERROR_SUCCESS; +} + +DWORD svcctl_OpenSCManagerW( + SvcCtlRpcHandle rpc_handle, + LPCWSTR MachineName, /* Note: this parameter is ignored */ + LPCWSTR DatabaseName, + DWORD dwAccessMask, + POLICY_HANDLE *handle) +{ + handle_data *data; + ULONG index; + WINE_TRACE("conn=%p (%s, %s, %x)\n", rpc_handle, wine_dbgstr_w(MachineName), wine_dbgstr_w(DatabaseName), dwAccessMask); + if (DatabaseName != NULL && DatabaseName[0]) + { + if (strcmpW(DatabaseName, SERVICES_FAILED_DATABASEW) == 0) + return ERROR_DATABASE_DOES_NOT_EXIST; + if (strcmpW(DatabaseName, SERVICES_ACTIVE_DATABASEW) != 0) + return ERROR_INVALID_NAME; + } + + if (!(data = (handle_data *)RtlAllocateHandle(&g_handle_table, &index))) + return ERROR_NOT_ENOUGH_SERVER_MEMORY; + ZeroMemory(data, sizeof(data)); + data->ulHandleFlags |= 1; /* mark handle as used */ + data->dwType = HANDLE_TYPE_MANAGER; + + if (dwAccessMask & MAXIMUM_ALLOWED) + dwAccessMask |= SC_MANAGER_ALL_ACCESS; + data->dwAccess = dwAccessMask; + RtlMapGenericMask(&data->dwAccess, &g_scm_generic); + + init_policy_handle(handle, index, data); + return ERROR_SUCCESS; +} + +DWORD svcctl_CloseServiceHandle( + SvcCtlRpcHandle rpc_handle, + POLICY_HANDLE *handle) +{ + handle_data *data; + DWORD err; + + WINE_TRACE("conn=%p (%s)\n", rpc_handle, wine_dbgstr_guid(&handle->uuid)); + if ((err = validate_policy_handle(handle, HANDLE_TYPE_DONT_CARE, 0, &data)) != ERROR_SUCCESS) + return err; + + RtlFreeHandle(&g_handle_table, (RTL_HANDLE *)data); + ZeroMemory(handle, sizeof(*handle)); + LeaveCriticalSection(&g_handle_table_cs); + return ERROR_SUCCESS; +} + +DWORD RPC_MainLoop(void) +{ + WCHAR transport[] = SVCCTL_TRANSPORT; + WCHAR endpoint[] = SVCCTL_ENDPOINT; + HANDLE hSleepHandle; + DWORD handle_size = 1; + DWORD err; + + while (handle_size < sizeof(handle_data)) + handle_size *= 2; /* handle size must be a power of 2 */ + RtlInitializeHandleTable(1048576, handle_size, &g_handle_table); + + if ((err = RpcServerUseProtseqEpW(transport, 0, endpoint, NULL)) != ERROR_SUCCESS) + { + WINE_ERR("RpcServerUseProtseq failed with error %u\n", err); + return err; + } + + if ((err = RpcServerRegisterIf(svcctl_v2_0_s_ifspec, 0, 0)) != ERROR_SUCCESS) + { + WINE_ERR("RpcServerRegisterIf failed with error %u", err); + return err; + } + + if ((err = RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, TRUE)) != ERROR_SUCCESS) + { + WINE_ERR("RpcServerListen failed with error %u\n", err); + return err; + } + + WINE_TRACE("Entered main loop\n"); + hSleepHandle = __wine_make_process_system(); + SetEvent(g_hStartedEvent); + do { + err = WaitForSingleObjectEx(hSleepHandle, INFINITE, TRUE); + WINE_TRACE("Wait returned %d\n", err); + } while (err != WAIT_OBJECT_0); + + WINE_TRACE("Object signaled - wine shutdown\n"); + return ERROR_SUCCESS; +} + +void __RPC_FAR * __RPC_USER MIDL_user_allocate(size_t len) +{ + return HeapAlloc(GetProcessHeap(), 0, len); +} + +void __RPC_USER MIDL_user_free(void __RPC_FAR * ptr) +{ + HeapFree(GetProcessHeap(), 0, ptr); +} diff --git a/programs/services/services.c b/programs/services/services.c index 74d4d2d..a94da2c 100644 --- a/programs/services/services.c +++ b/programs/services/services.c @@ -23,10 +23,12 @@ #define WIN32_LEAN_AND_MEAN #include #include #include +#include #include "wine/list.h" #include "wine/unicode.h" #include "wine/debug.h" +#include "svcctl.h" #include "services.h" @@ -34,6 +36,8 @@ #define MAX_SERVICE_NAME 260 WINE_DEFAULT_DEBUG_CHANNEL(services); +HANDLE g_hStartedEvent; + struct list g_services; /* Registry constants */ @@ -195,8 +199,9 @@ DWORD load_services() int main(int argc, char *argv[]) { DWORD err; + g_hStartedEvent = CreateEventW(NULL, TRUE, FALSE, SVCCTL_STARTED_EVENT); list_init(&g_services); if ((err = load_services()) != 0) return err; - return 0; + return RPC_MainLoop(); } diff --git a/programs/services/services.h b/programs/services/services.h index fd242ed..3ffe37c 100644 --- a/programs/services/services.h +++ b/programs/services/services.h @@ -34,6 +34,10 @@ typedef struct _service_entry LPWSTR dependOnGroups; } service_entry; +extern HANDLE g_hStartedEvent; + +DWORD RPC_MainLoop(void); + /* from utils.c */ LPWSTR strdupW(LPCWSTR str); diff --git a/programs/services/svcctl.idl b/programs/services/svcctl.idl new file mode 100644 index 0000000..101e0f2 --- /dev/null +++ b/programs/services/svcctl.idl @@ -0,0 +1 @@ +#include "wine/svcctl.idl" -- 1.4.1