Index: serve.c =================================================================== --- serve.c (revision 18556) +++ serve.c (working copy) @@ -2,7 +2,7 @@ * serve.c : Functions for serving the Subversion protocol * * ==================================================================== - * Copyright (c) 2000-2004 CollabNet. All rights reserved. + * Copyright (c) 2000-2006 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms Index: main.c =================================================================== --- main.c (revision 18556) +++ main.c (working copy) @@ -2,7 +2,7 @@ * main.c : Main control function for svnserve * * ==================================================================== - * Copyright (c) 2000-2004 CollabNet. All rights reserved. + * Copyright (c) 2000-2006 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms @@ -41,6 +41,7 @@ #include "svn_version.h" #include "svn_private_config.h" +#include "svn_winservice.h" #ifdef HAVE_UNISTD_H #include /* For getpid() */ @@ -63,6 +64,9 @@ run_mode_daemon, run_mode_tunnel, run_mode_listen_once +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + ,run_mode_service +#endif }; #if APR_HAS_FORK @@ -86,6 +90,51 @@ #endif +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT +/* + Ok, I KNOW this is horrible, but I don't see any other way (currently) + to safely signal the main thread to stop. + +*/ +#include + +static SOCKET winservice_accept_socket_handle; + +/* + +The SCM calls this function (on an arbitrary thread, not the main() thread!) +when it wants to stop the service. Right now, there is no clean way to stop +the service! So we just nuke the process. + +*/ +void winservice_notify_stop() +{ +#if 0 + dbg_print("winservice_notify_stop: terminating process!!"); + TerminateProcess(GetCurrentProcess(), ERROR_SUCCESS); +#else + HINSTANCE ws2_32; + typedef int (__stdcall* closesocket_proto)(SOCKET s); + closesocket_proto closesocket_ptr; + + + ws2_32 = GetModuleHandle(TEXT("WS2_32.DLL")); + if (ws2_32 != NULL) { + closesocket_ptr = (closesocket_proto)GetProcAddress(ws2_32, "closesocket"); + if (closesocket_ptr != NULL) { + dbg_print("rudely closing main's accept socket handle: %d", winservice_accept_socket_handle); + closesocket_ptr(winservice_accept_socket_handle); + } else { + dbg_print("could not find 'closesocket' on WS2_32.DLL"); + } + } else { + dbg_print("could not find WS2_32.DLL"); + } +#endif +} + +#endif /* ENABLE_WINDOWS_SERVICE_SUPPORT */ + /* Option codes and descriptions for svnserve. * * The entire list must be terminated with an entry of nulls. @@ -99,6 +148,9 @@ #define SVNSERVE_OPT_TUNNEL_USER 259 #define SVNSERVE_OPT_VERSION 260 #define SVNSERVE_OPT_PID_FILE 261 +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT +#define SVNSERVE_OPT_SERVICE 262 +#endif static const apr_getopt_option_t svnserve__options[] = { @@ -125,6 +177,9 @@ {"listen-once", 'X', 0, N_("listen once (useful for debugging)")}, {"pid-file", SVNSERVE_OPT_PID_FILE, 1, N_("write server process ID to file arg")}, +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + {"service", SVNSERVE_OPT_SERVICE, 0, N_("run as a windows service (SCM only)")}, +#endif {0, 0, 0, 0} }; @@ -171,8 +226,8 @@ return svn_opt_print_help(os, "svnserve", TRUE, FALSE, version_footer->data, NULL, NULL, NULL, NULL, pool); } - + #if APR_HAS_FORK static void sigchld_handler(int signo) { @@ -389,6 +444,13 @@ handling_mode = connection_mode_thread; break; +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + case SVNSERVE_OPT_SERVICE: + run_mode = run_mode_service; + mode_opt_count++; + break; +#endif + case SVNSERVE_OPT_PID_FILE: SVN_INT_ERR(svn_utf_cstring_to_utf8(&pid_filename, arg, pool)); pid_filename = svn_path_internal_style(pid_filename, pool); @@ -445,6 +507,43 @@ svn_error_clear(serve(conn, ¶ms, pool)); exit(0); } + +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + /* If svnserve needs to run as a Win32 service, then we need to coordinate + with the Service Control Manager (SCM) before continuing. This function + call registers the svnserve.exe process with the SCM, waits for the + "start" command from the SCM (which will come very quickly), and confirms + that those steps succeeded. + + After this call succeeds, the service is free to run. At some point in + the future, the SCM will send a message to the service, requesting that + it stop. This is translated into a call to winservice_notify_stop(). + The service is then responsible for cleanly terminating. + + We need to do this before actually starting the service logic (opening + files, sockets, etc.) because the SCM wants you to connect *first*, then + do your service-specific logic. If the service process takes too long + to connect to the SCM, then the SCM will decide that the service is + busted, and will give up on it. + */ + if (run_mode == run_mode_service) + { + err = winservice_start(); + if (err != NULL) + { + svn_handle_error2(err, stderr, FALSE, _("svnserve: ")); + svn_error_clear(err); + exit(1); + } + + /* The service is now in the "starting" state. Before the SCM will + consider the service "started", this thread must call the + winservice_running() function. + */ + + } + +#endif /* Make sure we have IPV6 support first before giving apr_sockaddr_info_get APR_UNSPEC, because it may give us back an IPV6 address even if we can't @@ -527,8 +626,23 @@ if (pid_filename) SVN_INT_ERR(write_pid_file(pid_filename, pool)); +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + /* At this point, the service is "running". Notify the SCM. */ + winservice_accept_socket_handle = sock->socketdes; + if (run_mode == run_mode_service) + winservice_running(); +#endif + while (1) { +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + if (winservice_is_stopping()) + { + dbg_print("main: exiting"); + exit(0); + } +#endif + /* Non-standard pool handling. The main thread never blocks to join the connection threads so it cannot clean up after each one. So separate pools, that can be cleared at thread exit, are used */ Index: svn_winservice.c =================================================================== --- svn_winservice.c (revision 0) +++ svn_winservice.c (revision 0) @@ -0,0 +1,464 @@ +/* + * svn_winservice.c : Implementation of Windows Service support + * + * ==================================================================== + * Copyright (c) 2000-2006 CollabNet. All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at http://subversion.tigris.org/license-1.html. + * If newer versions of this license are posted there, you may use a + * newer version instead, at your option. + * + * This software consists of voluntary contributions made by many + * individuals. For exact contribution history, see the revision + * history and logs, available at http://subversion.tigris.org/. + * ==================================================================== + */ + + + +#define APR_WANT_STRFUNC +#include +#include +#include +#include +#include +#include + +#include + +#include "svn_cmdline.h" +#include "svn_types.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_ra_svn.h" +#include "svn_utf.h" +#include "svn_path.h" +#include "svn_opt.h" +#include "svn_repos.h" +#include "svn_fs.h" +#include "svn_version.h" + +#include "svn_private_config.h" +#include "svn_winservice.h" + +#include + +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT +#include +#include + +/* This is just a placeholder, and doesn't actually constrain the service name. */ +#define WINSERVICE_SERVICE_NAME _("svnserve") + +static HANDLE winservice_dispatcher_thread = NULL; +static HANDLE winservice_start_event = NULL; +/* static HANDLE winservice_stop_event = NULL; */ +static SERVICE_STATUS_HANDLE winservice_status_handle = NULL; +static SERVICE_STATUS winservice_status; +static int winservice_accept_socket_handle; + +void dbg_print(const char* format, ...) +{ + va_list arglist; + char buffer[0x100]; + size_t len; + + va_start(arglist, format); + _vsnprintf(buffer, 0x100, format, arglist); + va_end(arglist); + + len = strlen(buffer); + while (len > 0 && (buffer[len - 1] == '\r' || buffer[len - 1] == '\n')) + buffer[--len] = 0; + + if (len+1 < 0x100) { + buffer[len++] = '\r'; + buffer[len] = 0; + } + + if (len+1 < 0x100) { + buffer[len++] = '\n'; + buffer[len] = 0; + } + + OutputDebugStringA(buffer); +} + +static void winservice_atexit(void); + +static void winservice_update_state() +{ + if (winservice_status_handle != NULL) + { + if (!SetServiceStatus(winservice_status_handle, &winservice_status)) { + dbg_print("SetServiceStatus - FAILED: %d\r\n", GetLastError()); + } + } +} + +/* + +This function cleans up state associated with the service support. +If the dispatcher thread handle is non-NULL, then this function +will wait for the dispatcher thread to exit. + +*/ +static void winservice_cleanup() +{ + if (winservice_start_event != NULL) { + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + } + + /* + if (winservice_stop_event != NULL) { + CloseHandle(winservice_stop_event); + winservice_stop_event = NULL; + } + */ + + if (winservice_dispatcher_thread != NULL) { + dbg_print("winservice_cleanup: waiting for dispatcher thread to exit..."); + WaitForSingleObject(winservice_dispatcher_thread, INFINITE); + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + } +} + +/* + +The SCM invokes this function to cause state changes in the service. + +*/ +static void WINAPI winservice_handler(DWORD control) +{ + switch (control) + { + case SERVICE_CONTROL_INTERROGATE: + /* The SCM just wants to check our state. We are required to call + SetServiceStatus, but we don't need to make any state changes. */ + dbg_print("SERVICE_CONTROL_INTERROGATE"); + winservice_update_state(); + break; + + case SERVICE_CONTROL_STOP: + dbg_print("SERVICE_CONTROL_STOP - Received request to stop service from SCM"); + winservice_status.dwCurrentState = SERVICE_STOP_PENDING; + winservice_update_state(); + winservice_notify_stop(); + break; + + default: + dbg_print("winservice_handler: received unknown control code: %u", control); + break; + } +} + +/* + +This is the "service main" routine (in the Win32 terminology). + +Normally, this function (thread) implements the "main" loop of a service. +However, in order to minimize changes to the svnserve main() function, this +function is running in a different thread, and main() is blocked in +winservice_start(), waiting for winservice_start_event. So this function +(thread) only needs to signal that event to "start" the service. + +If this function succeeds, it signals winservice_start_event, which wakes +up the winservice_start() frame that is blocked, + +*/ +static void WINAPI winservice_service_main(DWORD argc, LPTSTR* argv) +{ + DWORD error; + + assert(winservice_start_event != NULL); + + winservice_status_handle = RegisterServiceCtrlHandler(WINSERVICE_SERVICE_NAME, winservice_handler); + if (winservice_status_handle == NULL) + { + /* Ok, that's not fair. We received a request to start a service, and now we cannot + bind to the SCM in order to update status? Bring down the app. */ + error = GetLastError(); + dbg_print("RegisterServiceCtrlHandler FAILED: %d\r\n", GetLastError()); + /* Put the error code somewhere where winservice_start can find it. */ + winservice_status.dwWin32ExitCode = error; + SetEvent(winservice_start_event); + return; + } + + winservice_status.dwCurrentState = SERVICE_START_PENDING; + winservice_status.dwWin32ExitCode = ERROR_SUCCESS; + winservice_update_state(); + + dbg_print("winservice_service_main: signaling main thread to start (and returning)"); + SetEvent(winservice_start_event); +} + +const SERVICE_TABLE_ENTRY winservice_service_table[] = { + { WINSERVICE_SERVICE_NAME, winservice_service_main }, + { NULL, NULL } +}; + +/* + +This is the thread routine for the "dispatcher" thread. The purpose of this thread +is to connect this process with the Service Control Manager, which allows this +process to receive control requests from the SCM, and allows this process to +update the SCM with status information. + +The StartServiceCtrlDispatcher connects this process to the SCM. If it succeeds, +then it will not return until all of the services running in this process have +stopped. (In our case, there is only one service per process.) + +*/ +static DWORD WINAPI winservice_dispatcher_thread_routine(PVOID arg) +{ + dbg_print("winservice_dispatcher_thread_routine: starting"); + + if (!StartServiceCtrlDispatcher(winservice_service_table)) + { + /* This is a common error. Usually, it means the user has invoked the service + with the --service flag directly. This is incorrect. The only time the + --service flag is passed is when the process is being started by the SCM. + */ + DWORD error = GetLastError(); + dbg_print("winservice_dispatcher_thread_routine: FAILED to connect to SCM: %d", error); + return error; + } + + dbg_print("winservice_dispatcher_thread_routine: SCM is done using this process -- exiting"); + return ERROR_SUCCESS; +} + +/* + +This function looks up the error text for a Win32 error code. +These are in and are returned from GetLastError(). +If the error code cannot be found, "Unknown error: %u" is +written to the buffer. + +*/ +static void get_win32_error_text(DWORD error, LPTSTR buffer, int maxlength) +{ + int length; + + length = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, error, LANG_NEUTRAL, buffer, maxlength, NULL); + if (length != 0) { + /* FormatMessage does not terminate strings, oddly enough. */ + buffer[length] = 0; + } else { + _sntprintf(buffer, maxlength, _("Unknown error: %u"), error); + } +} + +/* + +This function creates an svn_error, given a Win32 error code and an optional message. + +*/ +static svn_error_t* svn_error_create_win32(DWORD error, svn_error_t* child, LPCTSTR message) +{ +#define BUFFER_LEN 0x100 + + TCHAR buffer[BUFFER_LEN]; + int pos; + + if (message != NULL) { + _tcsncpy(buffer, message, BUFFER_LEN); + _tcsncat(buffer, _(": "), BUFFER_LEN); + buffer[BUFFER_LEN - 1] = 0; + } else { + buffer[0] = 0; + } + + pos = (int)_tcslen(buffer); + get_win32_error_text(error, &buffer[pos], BUFFER_LEN - pos); + + return svn_error_create(0, child, buffer); + +#undef BUFFER_LEN +} + + /* If svnserve needs to run as a Win32 service, then we need to coordinate + with the Service Control Manager (SCM) before continuing. This function + call registers the svnserve.exe process with the SCM, waits for the + "start" command from the SCM (which will come very quickly), and confirms + that those steps succeeded. + + After this call succeeds, the service is free to run. At some point in + the future, the SCM will send a message to the service, requesting that + it stop. This is translated into a call to winservice_stop(). + The service is then responsible for cleanly terminating. Before exiting, + the service should call winservice_exiting(). +*/ +svn_error_t* winservice_start() +{ + HANDLE handles[2]; + DWORD thread_id; + DWORD status; + DWORD error; + + dbg_print("winservice_start: starting svnserve as a service..."); + + ZeroMemory(&winservice_status, sizeof(winservice_status)); + + winservice_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + winservice_status.dwControlsAccepted = SERVICE_ACCEPT_STOP; + winservice_status.dwCurrentState = SERVICE_STOPPED; + + /* Create the event that will wake up this thread when we receive SERVICE_START control message. */ + winservice_start_event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (winservice_start_event == NULL) + { + error = GetLastError(); + return svn_error_create_win32(error, NULL, "The service failed to start"); + } + + winservice_dispatcher_thread = (HANDLE)_beginthreadex(NULL, 0, winservice_dispatcher_thread_routine, NULL, 0, &thread_id); + if (winservice_dispatcher_thread == NULL) + { + error = GetLastError(); + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + return svn_error_create_win32(0, NULL, "The service failed to start"); + } + + /* Next, we wait for the "start" event to fire (meaning the service logic has + successfully started), or for the dispatch thread to exit (meaning the service + logic could not start). */ + + handles[0] = winservice_start_event; + handles[1] = winservice_dispatcher_thread; + status = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + dbg_print("winservice_start: service is now starting\r\n"); + + /* We no longer need the start event. */ + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + + /* Register our cleanup logic. */ + atexit(winservice_atexit); + return SVN_NO_ERROR; + + case WAIT_OBJECT_0+1: + /* The dispatcher thread exited without starting the service. + This happens when the dispatcher fails to connect to the SCM. */ + dbg_print("winservice_start: dispatcher thread failed to start service"); + if (GetExitCodeThread(winservice_dispatcher_thread, &error)) { + dbg_print("winservice_start: dispatcher thread failed to start service: %d", error); + if (error == ERROR_SUCCESS) + error = ERROR_INTERNAL_ERROR; + } else { + dbg_print("winservice_start: failed to get exit code from dispatcher thread: %d", GetLastError()); + error = ERROR_INTERNAL_ERROR; + } + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + + winservice_cleanup(); + + return svn_error_create_win32(error, NULL, "The service failed to start"); + + default: + /* This should never happen! This indicates that our handles are broken, or some other + highly unusual error. There is nothing rational that we can do to recover. */ + error = GetLastError(); + dbg_print("winservice_start: internal error!! WaitForMultipleObjects failed! status: %d error: %d", status, error); + + winservice_cleanup(); + return svn_error_create(0, NULL, "The service could not be started; a serious internal error occurred while starting the service."); + } +} + +/* + +main() calls this function in order to inform the SCM that the service has successfully +started. This is required; otherwise, the SCM will believe that the service is stuck +in the "starting" state, and management tools will also believe that the service is stuck. + +*/ + +void winservice_running() +{ + dbg_print("winservice_notify_running: updating state to SERVICE_RUNNING"); + winservice_status.dwCurrentState = SERVICE_RUNNING; + winservice_update_state(); +} + +/* + +main() calls this function in order to notify the SCM that the service has stopped. +This function also handles cleaning up the dispatcher thread (the one that we created +above in winservice_start. + +*/ +void winservice_stop(DWORD exit_code) +{ + dbg_print("winservice_stop"); + winservice_status.dwCurrentState = SERVICE_STOPPED; + winservice_status.dwWin32ExitCode = exit_code; + winservice_update_state(); + + if (winservice_dispatcher_thread != NULL) { + dbg_print("waiting for dispatcher thread to exit..."); + WaitForSingleObject(winservice_dispatcher_thread, INFINITE); + dbg_print("dispatcher thread has exited."); + + if (GetExitCodeThread(winservice_dispatcher_thread, &exit_code)) { + if (exit_code == ERROR_SUCCESS) { + dbg_print("dispatcher exit code indicates success."); + } else { + dbg_print("dispatcher exit code indicates failure: %d", exit_code); + } + } else { + dbg_print("Failed to get exit code for dispatcher thread!"); + exit_code = GetLastError(); + } + + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + } else { + /* There was no dispatcher thread. So we never started in the first place. */ + exit_code = winservice_status.dwWin32ExitCode; + dbg_print("dispatcher thread was not running!"); + } + + if (winservice_start_event != NULL) { + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + } + + /* + if (winservice_stop_event != NULL) { + CloseHandle(winservice_stop_event); + winservice_stop_event = NULL; + } + */ + + dbg_print("winservice_stop - returning, exit_code %d", exit_code); +} + + + +/* +This function is installed as an atexit-handler. +This is done so that we don't need to alter every exit() call in main(). +*/ +static void winservice_atexit(void) +{ + dbg_print("winservice_atexit - stopping"); + winservice_stop(ERROR_SUCCESS); +} + +int winservice_is_stopping() +{ + return winservice_status.dwCurrentState == SERVICE_STOP_PENDING; +} + +#endif Index: svn_winservice.h =================================================================== --- svn_winservice.h (revision 0) +++ svn_winservice.h (revision 0) @@ -0,0 +1,33 @@ +/* + * svn_winservice.h : Public definitions for Windows Service support + * + * ==================================================================== + * Copyright (c) 2000-2004 CollabNet. All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at http://subversion.tigris.org/license-1.html. + * If newer versions of this license are posted there, you may use a + * newer version instead, at your option. + * + * This software consists of voluntary contributions made by many + * individuals. For exact contribution history, see the revision + * history and logs, available at http://subversion.tigris.org/. + * ==================================================================== + */ + + + +#ifdef _WIN32 +#define ENABLE_WINDOWS_SERVICE_SUPPORT +#endif + + + +extern svn_error_t* winservice_start(); +extern void winservice_stop(DWORD exit_code); +extern void winservice_running(); +extern void winservice_notify_stop(); +void dbg_print(const char* format, ...); +int winservice_is_stopping(); +