/* * winservice.c : Implementation of Windows Service support * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #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(void) { 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(void) { 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(void) { 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)CreateThread(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(void) { 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. */ static 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(void) { return (winservice_status.dwCurrentState == SERVICE_STOP_PENDING); } #endif /* WIN32 */