Index: notes/windows-service.txt =================================================================== --- notes/windows-service.txt (revision 0) +++ notes/windows-service.txt (revision 0) @@ -0,0 +1,203 @@ + +Windows Service Support for svnserve +------------------------------------ + +svnserve can now be run as a native Windows service. This means that the +service can be started at system boot, or at any other time, without the +need for any wrapper code to start the service. The service can be managed +like any other Windows service, using command-line tools ("net start", +"net stop", or sc.exe) or GUI tools (the Services administrative tool). + + +Installation +------------ + +For now, no means is provided to install the service. Windows XP and +Windows 2003 Server provide a command-line tool for installing services. +To create a service for svnserve, invoke SC.EXE like so: + + sc create + binpath= "c:\svn\bin\svnserve.exe --service " + depend= Tcpip + +where is any service name you want, e.g. "svnserve", and +are the arguments to svnserve, such as --root, --listen-port, etc. +(All of this should be specified on a single line, of course.) + +In order for svnserve to run as a Windows service, you MUST specify the +--service argument, and you must NOT specify any other run mode argument, +such as --daemon, --tunnel, --inetd, or any of their short forms. There +is no short form for --service. + +SC has many options; use "sc /?". The most useful are: + + sc create create a new service + sc qc query config for a service + sc query query status + sc delete delete any service -- BE CAREFUL! + sc config ... update service config; same args as sc create + sc start start a service (does NOT wait for completion!) + sc stop stop a service (does NOT wait for it to stop!) + +Note that the command-line syntax for SC is rather odd. Key/value +pairs are specified as "key= value". The "key=" part must not have +any spaces, and the "value" part MUST be separated from the "key=" +by a space. + +If you want to be able to see the command shell, add these arguments +to the "sc create" command-line: + + type= own type= interact + +This sets the "interactive" bit on the service, which allows it to interact +with the local console session. + +You can create as many services as you like; there is no restriction on +the number of services, or their names. I use a prefix, like "svn.foo", +"svn.bar", etc. Each service runs in a separate process. + + +Uninstalling +------------ + +To uninstall a service, stop the service, then delete it, using +"sc delete ". Be very careful with this command, since you can +delete any system service, including essential Windows services, +accidentally. + +Also, make sure that you stop the service before you delete it. +If you delete the service before stopping it, the Service Control +Manager will mark the service "deleted", but will intentionally +not stop the service. The service will be deleted when the system +reboots, or when the service finally exits. After all, you only +asked to delete the service, not to stop it. + +That's all there is to it. + + +Automatically Starting Service on System Boot +--------------------------------------------- + +By default, SC creates the service with the start mode set to "demand" +(manual). If you want the service to start automatically when the system +boots, add "start= auto" to the command line. You can change the start +mode for an existing service using "sc config start= auto", or +also by using the Windows GUI interface for managing services. (Start, +All Programs, Administrative Tools, Services, or just run "services.msc" +from Start/Run or from a command-line.) + +Note: In order for svnserve to start correctly on system boot, you must +properly declare its startup dependencies. The Service Control Manager +will start services as early as it can, and if you do not properly +declare its startup dependencies, it can potentially start before the +TCP/IP stack has been started. This is why you must provide specify +'depend= Tcpip' to SC.EXE when creating the service. + + +Starting and Stopping the Service +--------------------------------- + +You start and stop the service like any other Windows service. You can +use the command-line "net start ", use the GUI Services interface. + + +Debugging +--------- + +Debugging a Windows service can be difficult, because the service runs in +a very different context than a user who is logged in. By default, services +run in a "null" desktop environment. They cannot interact with the user +(desktop) in any way, and vice versa. + +Also, by default, services run as a special user, called LocalSystem. +LocalSystem is not a "user" in the normal sense; it is an NT security ID (SID) +that is sort of like root, but different. LocalSystem typically does NOT +have access to any network shares, even if you use "net use" to connect +to a remote file server. Again, this is because services run in a different +login session. + +Depending on which OS you are running, you may have difficulty attaching +a debugger to a running service process. Also, if you are having trouble +*starting* a service, then you can't attach to the process early enough to +debug it. + +So what's a developer to do? Well, there are several ways you can debug +services. First, you'll want to enable "interactive" access for the service. +This allows the service to interact with the local desktop -- you'll be able +to see the command shell that the service runs in, see console output, etc. +To do this, you can either use the standard Windows Services tool (services.msc), +or you can do it using sc.exe. + + * With the GUI tool, open the properties page for a service, and go + to the "Log On" page. Select "Local System account", and make sure + the "Allow service to interact with desktop" box is checked. + + * With SC.EXE, configure the service using the command: + + sc config type= own type= interact + + Yes, you must specify type= twice, and with exactly the spacing shown. + +In both cases, you'll need to restart the service. When you do, if the +service started successfully, you'll see the console window of the service. +By default, it doesn't print anything out. + +Next, you'll want to attach a debugger, or configure the service to start +under a debugger. Attaching a debugger should be straightforward -- just +find the process ID. But if you need to debug something in the service +startup path, you'll need to have a debugger attached from the very beginning. +There are two ways to do this. + +In the first method, you alter the command-line of the service (called the +"binary path"). To do this, use SC.EXE to set the binary path to whatever +debugger you are going to use. I use the most recent version of WinDbg, +which is excellent, and is available at: + + http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx + +For example, this command would configure the service to start under a +debugger: + + sc config binpath= "d:\dbg\windbg.exe -g -G d:\svn\bin\svnserve.exe + --root d:\path\root --listen-port 9000" + depend= Tcpip + +The entire command must be on a single line, of course, and the binary path +must be in double-quotes. Also, the spacing MUST be: binpath= "..." + +Substitute whatever debugger you want, with whatever command-line you want, +in place of windbg.exe. Then start the service (sc start ), and the +Service Control Manager should execute the command-line you provided as +the binary path. Then your debugger should start, and should launch the +svnserve process. + + +Known Issues +------------ + +* No management tool (installer, etc.). For the first revision, this is + intentional; we just want to get the service functionality tested and + committed before dealing with installation. + +* Right now, I don't know of a way to cleanly stop the svnserve process. + Instead, the implementation closes the listen socket, which causes the + main loop to exit. This isn't as bad as it sounds, and is a LOT better + than other options (such as terminating a thread). + + +Future Changes +-------------- + +The support for running svnserve as a Windows service is complete, but +there is still more work to be done for installing and managing services. +I plan on providing GUI and command-line tools for doing this, but this +work is not part of the first version of service support. + + +Contact +------- + +This support was written by Arlie Davis, February 2006. You can reach me +at my permanent email address, arlie@sublinear.org, and also at my current +work email address at, adavis@stonestreetone.com. + Index: subversion/svnserve/winservice.c =================================================================== --- subversion/svnserve/winservice.c (revision 0) +++ subversion/svnserve/winservice.c (revision 0) @@ -0,0 +1,539 @@ +/* + * 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 + +#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 "winservice.h" + +/* + +Design Notes +------------ + +The code in this file allows svnserve to run as a Windows service. +Windows Services are only supported on operating systems derived +from Windows NT, which is basically all modern versions of Windows +(2000, XP, Server, Vista, etc.) and excludes the Windows 9x line. + +Windows Services are processes that are started and controlled by +the Service Control Manager. When the SCM wants to start a service, +it creates the process, then waits for the process to connect to +the SCM, so that the SCM and service process can communicate. +This is done using the StartServiceCtrlDispatcher function. + +In order to minimize changes to the svnserve startup logic, this +implementation differs slightly from most service implementations. +In most services, main() immediately calls StartServiceCtrlDispatcher, +which does not return control to main() until the SCM sends the +"stop" request to the service, and the service stops. + + +Installing the Service +--------------------- + +Installation is beyond the scope of source code comments. There +is a separate document that describes how to install and uninstall +the service. Basically, you create a Windows Service, give it a +binary path that points to svnserve.exe, and make sure that you +specify --service on the command line. + + +Starting the Service +-------------------- + +First, the SCM decides that it wants to start a service. It creates +the process for the service, passing it the command-line that is +stored in the service configuration (stored in the registry). + +Next, main() runs. The command-line should contain the --service +argument, which is the hint that svnserve is running under the SCM, +not as a standalone process. main() calls winservice_start(). + +winservice_start() creates an event object (winservice_start_event), +and creates and starts a separate thread, the "dispatcher" thread. +winservice_start() then waits for either winservice_start_event +to fire (meaning: "the dispatcher thread successfully connected +to the SCM, and now the service is starting") or for the dispatcher +thread to exit (meaning: "failed to connect to SCM"). + +If the dispatcher thread quit, then winservice_start returns an error. +If the start event fired, then winservice_start returns a success code +(SVN_NO_ERROR). At this point, the service is now in the "starting" +state, from the perspective of the SCM. winservice_start also registers +an atexit handler, which handles cleaning up some of the service logic, +as explained below in "Stopping the Service". + +Next, control returns to main(), which performs the usual startup +logic for svnserve. Mostly, it creates the listener socket. If +main() was able to start the service, then it calls the function +winservice_running(). + +winservice_running() informs the SCM that the service has finished +starting, and is now in the "running" state. main() then does its +work, accepting client sockets and processing SVN requests. + +Stopping the Service +-------------------- + +At some point, the SCM will decide to stop the service, either because +an administrator chose to stop the service, or the system is shutting +down. To do this, the SCM calls winservice_handler() with the +SERVICE_CONTROL_STOP control code. When this happens, +winservice_handler() will inform the SCM that the service is now +in the "stopping" state, and will call winservice_notify_stop(). + +winservice_notify_stop() is responsible for cleanly shutting down the +svnserve logic (waiting for client requests to finish, stopping database +access, etc.). Right now, all it does is close the listener socket, +which causes the apr_socket_accept() call in main() to fail. main() +then calls exit(), which processes all atexit() handlers, which +results in winservice_stop() being called. + +winservice_stop() notifies the SCM that the service is now stopped, +and then waits for the dispatcher thread to exit. Because all services +in the process have now stopped, the call to StartServiceCtrlDispatcher +(in the dispatcher thread) finally returns, and winservice_stop() returns, +and the process finally exits. + +-- +Arlie Davis +arlie@sublinear.org + +*/ + + +#ifdef ENABLE_WINDOWS_SERVICE_SUPPORT + +#include +#include +#include + +/* + +This is just a placeholder, and doesn't actually constrain the service name. +You have to provide *some* service name to the SCM API, but for services that +are marked SERVICE_WIN32_OWN_PROCESS, the service name is ignored. It *is* +relevant for service binaries that run more than one service in a single +process. + +*/ +#define WINSERVICE_SERVICE_NAME _("svnserve") + +/* + +Global Variables +---------------- + +winservice_dispatcher_thread + Win32 handle to the dispatcher thread. + +winservice_start_event + Win32 event handle, used to notify winservice_start() that we have + successfully connected to the SCM. + +winservice_status_handle + Handle that allows us to notify the SCM of changes in our service status. + +winservice_status + The current status of the service (stopped, running, controls accepted, + exit code, etc.) + +*/ +static HANDLE winservice_dispatcher_thread = NULL; +static HANDLE winservice_start_event = NULL; +static SERVICE_STATUS_HANDLE winservice_status_handle = NULL; +static SERVICE_STATUS winservice_status; + +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_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); +} + +static 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; +} + + /* 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 should perform whatever work it needs + to start the service, and then the service should call winservice_running() + (if no errors occurred) or winservice_stop() (if something failed during + startup). +*/ +svn_error_t* winservice_start() +{ + HANDLE handles[2]; + DWORD thread_id; + DWORD error_code; + apr_status_t apr_status; + DWORD wait_status; + + 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) + { + apr_status = apr_get_os_error(); + return svn_error_wrap_apr(apr_status, _("Failed to create winservice_start_event")); + } + + winservice_dispatcher_thread = (HANDLE)_beginthreadex(NULL, 0, winservice_dispatcher_thread_routine, NULL, 0, &thread_id); + if (winservice_dispatcher_thread == NULL) + { + apr_status = apr_get_os_error(); + winservice_cleanup(); + return svn_error_wrap_apr(apr_status, "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; + wait_status = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + switch (wait_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_code)) { + dbg_print("winservice_start: dispatcher thread failed to start service: %d", error_code); + + if (error_code == ERROR_SUCCESS) + error_code = ERROR_INTERNAL_ERROR; + + } else { + dbg_print("winservice_start: failed to get exit code from dispatcher thread: %d", GetLastError()); + error_code = ERROR_INTERNAL_ERROR; + } + + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + + winservice_cleanup(); + + return svn_error_wrap_apr(APR_FROM_OS_ERROR(error_code), _("Failed to connect to Service Control Manager")); + + 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. */ + apr_status = apr_get_os_error(); + dbg_print("winservice_start: internal error!! WaitForMultipleObjects failed! status: %d error: %d", wait_status, GetLastError()); + + winservice_cleanup(); + return svn_error_wrap_apr(apr_status, _("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: subversion/svnserve/winservice.h =================================================================== --- subversion/svnserve/winservice.h (revision 0) +++ subversion/svnserve/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(); + Index: subversion/svnserve/main.c =================================================================== --- subversion/svnserve/main.c (revision 18609) +++ subversion/svnserve/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 "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,56 @@ #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. + +For now, our strategy is to close the listener socket, in order to unblock +main() and cause it to exit its accept loop. We cannot use apr_socket_close, +because that function deletes the apr_socket_t structure, as well as closing +the socket handle. If we called apr_socket_close here, then main() will also +call apr_socket_close, resulting in a double-free. This way, we just close +the kernel socket handle, which causes the accept() function call to fail, +which causes main() to clean up the socket. So, memory gets freed only once. + +This isn't pretty, but it's better than a lot of other options. Currently, +there is no "right" way to shut down svnserve. + +*/ +void winservice_notify_stop() +{ + 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 /* ENABLE_WINDOWS_SERVICE_SUPPORT */ + + /* Option codes and descriptions for svnserve. * * The entire list must be terminated with an entry of nulls. @@ -99,6 +153,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 +182,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 +231,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 +449,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 +512,52 @@ 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: ")); + + /* This is the most common error. It means the user started svnserve from a shell, and specified + the --service argument. svnserve cannot be started, as a service, in this way. The --service + argument is valid only valid if svnserve is started by the SCM. */ + if (err->apr_err == APR_FROM_OS_ERROR(ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)) { + svn_error_clear(svn_cmdline_fprintf(stderr, pool, + _("svnserve: The --service flag is only valid if the process is started by the Service Control Manager.\n"))); + } + + 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 +640,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 */