Index: notes/windows-service.txt =================================================================== --- notes/windows-service.txt (revision 0) +++ notes/windows-service.txt (revision 0) @@ -0,0 +1,207 @@ + +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. Most Windows OSes +derived from Windows NT (such as Windows XP, Windows 2000, Windows 2003 +Server) provide a command-line tool for installing services, called SC.EXE +for "Service Control". To create a service for svnserve, use SC.EXE: + + sc create + binpath= "c:\svn\bin\svnserve.exe --service " + displayname= "Subversion Repository" + 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. + +If the path to svnserve.exe contains spaces or other characters that must +be escaped, then you must enclose the path to svnserve.exe with double- +quotes, which themselves must be quoted using a backslash. Fortunately +the syntax is similar to that on Unix platforms: + + sc create + binpath= "\"c:\program files\subversion\bin\svnserve.exe\" ..." + +SC has many options; use "sc /?". The most relevant 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" (without the double-quotes). 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 need; 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. As usual, +it is your responsbility as an administrator to make sure that no +two service instances use the same repository root path, or the same +combination of --listen-port and --listen-host. + + +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 version, 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). + + +To Do +----- + +* 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. + + Index: subversion/svnserve/winservice.c =================================================================== --- subversion/svnserve/winservice.c (revision 0) +++ subversion/svnserve/winservice.c (revision 0) @@ -0,0 +1,485 @@ +/* + * 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 "svn_error.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. + +*/ + + +#ifdef _WIN32 + +#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 (as is the case for svnserve), +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" + + +/* Win32 handle to the dispatcher thread. */ +static HANDLE winservice_dispatcher_thread = NULL; + +/* Win32 event handle, used to notify winservice_start() that we have + successfully connected to the SCM. */ +static HANDLE winservice_start_event = NULL; + +/* RPC handle that allows us to notify the SCM of changes in our + service status. */ +static SERVICE_STATUS_HANDLE winservice_status_handle = NULL; + +/* Our current idea of the service status (stopped, running, + controls accepted, exit code, etc.) */ +static SERVICE_STATUS winservice_status; + + +#ifdef SVN_DEBUG + +static void dbg_print(const char* text) +{ + OutputDebugStringA(text); +} + +#else + +/* Make sure dbg_print compiles to nothing in release builds. */ +#define dbg_print(text) + +#endif + +static void winservice_atexit(void); + +/* Notifies the Service Control Manager of the current state of the + service. */ +static void winservice_update_state() +{ + if (winservice_status_handle != NULL) + { + if (!SetServiceStatus(winservice_status_handle, &winservice_status)) { + dbg_print("SetServiceStatus - FAILED\r\n"); + } + } +} + +/* + +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\r\n"); + 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\r\n"); + winservice_update_state(); + break; + + case SERVICE_CONTROL_STOP: + dbg_print("SERVICE_CONTROL_STOP\r\n"); + winservice_status.dwCurrentState = SERVICE_STOP_PENDING; + winservice_update_state(); + winservice_notify_stop(); + 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\r\n"); + /* 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: service is starting\r\n"); + 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\r\n"); + + 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("dispatcher: FAILED to connect to SCM\r\n"); + return error; + } + + dbg_print("dispatcher: SCM is done using this process -- exiting\r\n"); + 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...\r\n"); + + 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 the SCM creates + the ServiceMain thread. */ + 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 has failed\r\n"); + + if (GetExitCodeThread(winservice_dispatcher_thread, &error_code)) { + dbg_print("winservice_start: dispatcher thread failed\r\n"); + + if (error_code == ERROR_SUCCESS) + error_code = ERROR_INTERNAL_ERROR; + + } else { + 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: WaitForMultipleObjects failed!\r\n"); + + winservice_cleanup(); + return svn_error_wrap_apr(apr_status,_("The service failed to start; " + "an 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() +{ + winservice_status.dwCurrentState = SERVICE_RUNNING; + winservice_update_state(); + dbg_print("winservice_notify_running: service is now running\r\n"); +} + +/* + +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 - notifying SCM that service has stopped\r\n"); + 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...\r\n"); + WaitForSingleObject(winservice_dispatcher_thread, INFINITE); + dbg_print("dispatcher thread has exited.\r\n"); + + 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\r\n"); + } + + if (winservice_start_event != NULL) { + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + } + + dbg_print("winservice_stop - service has stopped\r\n"); +} + + + +/* +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\r\n"); + winservice_stop(ERROR_SUCCESS); +} + +svn_boolean_t winservice_is_stopping() +{ + return winservice_status.dwCurrentState == SERVICE_STOP_PENDING; +} + +#endif /* _WIN32 */ Index: subversion/svnserve/winservice.h =================================================================== --- subversion/svnserve/winservice.h (revision 0) +++ subversion/svnserve/winservice.h (revision 0) @@ -0,0 +1,53 @@ +/* + * 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 + +/* Connects to the Windows Service Control Manager and allows this + process to run as a service. This function can only succeed if + the process was started by the SCM, not directly by a user. + 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(); + +/* Notifies the SCM that the service has stopped, and cleans up any + state associated with service support. If winservice_running + is called, then this function is not necessary, because winservice_start + registers an atexit hander that calls this function. */ +void winservice_stop(DWORD exit_code); + +/* Notifies the SCM that the service is now running. The caller must + already have called winservice_start successfully. */ +void winservice_running(); + +/* This function is called by the SCM in an arbitrary thread when the + SCM wants the service to stop. The implementation of this function + can return immediately; all that is necessary is that the service + eventually stop in response. */ +void winservice_notify_stop(); + +/* Evaluates to TRUE if the SCM has requested that the service stop. + This allows for the service to poll, in addition to being notified + in the winservice_notify_stop callback. */ +svn_boolean_t winservice_is_stopping(); + +#endif /* _WIN32 */ Index: subversion/svnserve/main.c =================================================================== --- subversion/svnserve/main.c (revision 18642) +++ 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 @@ -25,6 +25,7 @@ #include #include #include +#include #include @@ -41,6 +42,7 @@ #include "svn_version.h" #include "svn_private_config.h" +#include "winservice.h" #ifdef HAVE_UNISTD_H #include /* For getpid() */ @@ -62,7 +64,8 @@ run_mode_inetd, run_mode_daemon, run_mode_tunnel, - run_mode_listen_once + run_mode_listen_once, + run_mode_service }; #if APR_HAS_FORK @@ -86,6 +89,41 @@ #endif +#ifdef _WIN32 +#include + +static HANDLE 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. + +We store the OS handle rather than a pointer to the apr_socket_t structure +in order to eliminate any possibility of illegal memory access. + +*/ + +void winservice_notify_stop() +{ + CloseHandle((HANDLE)winservice_accept_socket_handle); +} + +#endif /* _WIN32 */ + + /* Option codes and descriptions for svnserve. * * The entire list must be terminated with an entry of nulls. @@ -99,6 +137,7 @@ #define SVNSERVE_OPT_TUNNEL_USER 259 #define SVNSERVE_OPT_VERSION 260 #define SVNSERVE_OPT_PID_FILE 261 +#define SVNSERVE_OPT_SERVICE 262 static const apr_getopt_option_t svnserve__options[] = { @@ -125,6 +164,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 _WIN32 + {"service", SVNSERVE_OPT_SERVICE, 0, N_("run as a windows service (SCM only)")}, +#endif {0, 0, 0, 0} }; @@ -171,8 +213,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 +431,13 @@ handling_mode = connection_mode_thread; break; +#ifdef _WIN32 + 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 +494,56 @@ svn_error_clear(serve(conn, ¶ms, pool)); exit(0); } + +#ifdef _WIN32 + /* 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 /* _WIN32 */ /* 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,26 @@ if (pid_filename) SVN_INT_ERR(write_pid_file(pid_filename, pool)); +#ifdef _WIN32 + /* At this point, the service is "running". Notify the SCM. */ + status = apr_os_sock_get( + (apr_os_sock_t*)&winservice_accept_socket_handle, sock); + if (!APR_STATUS_IS_SUCCESS(status)) + winservice_accept_socket_handle = NULL; + + if (run_mode == run_mode_service) + winservice_running(); +#endif + while (1) { +#ifdef _WIN32 + if (winservice_is_stopping()) + { + 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 */