// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // This file implements the Windows service controlling Me2Me host processes // running within user sessions. #include "remoting/host/host_service_win.h" #include #include #include #include "base/at_exit.h" #include "base/base_paths.h" #include "base/bind.h" #include "base/command_line.h" #include "base/file_util.h" #include "base/logging.h" #include "base/message_loop.h" #include "base/path_service.h" #include "base/stringprintf.h" #include "base/threading/thread.h" #include "base/utf_string_conversions.h" #include "base/win/wrapped_window_proc.h" #include "remoting/base/scoped_sc_handle_win.h" #include "remoting/host/branding.h" #include "remoting/host/host_service_resource.h" #include "remoting/host/wts_console_observer_win.h" #include "remoting/host/wts_session_process_launcher_win.h" using base::StringPrintf; namespace { // Service name. const char kServiceName[] = "chromoting"; // TODO(alexeypa): investigate and migrate this over to Chrome's i18n framework. const char kMuiStringFormat[] = "@%ls,-%d"; const char kServiceDependencies[] = ""; const char kServiceCommandLineFormat[] = "\"%ls\" --host-binary=\"%ls\""; const DWORD kServiceStopTimeoutMs = 30 * 1000; // Session id that does not represent any session. const uint32 kInvalidSession = 0xffffffff; const char kIoThreadName[] = "I/O thread"; // A window class for the session change notifications window. static const char kSessionNotificationWindowClass[] = "Chromoting_SessionNotificationWindow"; // Command line actions and switches: // "run" sumply runs the service as usual. const char kRunActionName[] = "run"; // "install" requests the service to be installed. const char kInstallActionName[] = "install"; // "remove" uninstalls the service. const char kRemoveActionName[] = "remove"; // "--console" runs the service interactively for debugging purposes. const char kConsoleSwitchName[] = "console"; // "--host-binary" specifies the host binary to run in console session. const char kHostBinarySwitchName[] = "host-binary"; // "--help" or "--?" prints the usage message. const char kHelpSwitchName[] = "help"; const char kQuestionSwitchName[] = "?"; const char kUsageMessage[] = "\n" "Usage: %s [action] [options]\n" "\n" "Actions:\n" " run - Run the service (default if no action was specified).\n" " install - Install the service.\n" " remove - Uninstall the service.\n" "\n" "Options:\n" " --console - Run the service interactively for debugging purposes.\n" " --host-binary - Specifies the host binary to run in the console session.\n" " --help, --? - Print this message.\n"; // Exit codes: const int kSuccessExitCode = 0; const int kUsageExitCode = 1; const int kErrorExitCode = 2; void usage(const char* program_name) { fprintf(stderr, kUsageMessage, program_name); } } // namespace namespace remoting { HostService::HostService() : console_session_id_(kInvalidSession), message_loop_(NULL), run_routine_(&HostService::RunAsService), service_name_(ASCIIToUTF16(kServiceName)), service_status_handle_(0), shutting_down_(false), stopped_event_(true, false) { } HostService::~HostService() { } void HostService::AddWtsConsoleObserver(WtsConsoleObserver* observer) { console_observers_.AddObserver(observer); } void HostService::RemoveWtsConsoleObserver(WtsConsoleObserver* observer) { console_observers_.RemoveObserver(observer); } void HostService::OnSessionChange() { // WTSGetActiveConsoleSessionId is a very cheap API. It basically reads // a single value from shared memory. Therefore it is better to check if // the console session is still the same every time a session change // notification event is posted. This also takes care of coalescing multiple // events into one since we look at the latest state. uint32 console_session_id = kInvalidSession; if (!shutting_down_) { console_session_id = WTSGetActiveConsoleSessionId(); } if (console_session_id_ != console_session_id) { if (console_session_id_ != kInvalidSession) { FOR_EACH_OBSERVER(WtsConsoleObserver, console_observers_, OnSessionDetached()); } console_session_id_ = console_session_id; if (console_session_id_ != kInvalidSession) { FOR_EACH_OBSERVER(WtsConsoleObserver, console_observers_, OnSessionAttached(console_session_id_)); } } } BOOL WINAPI HostService::ConsoleControlHandler(DWORD event) { HostService* self = HostService::GetInstance(); switch (event) { case CTRL_C_EVENT: case CTRL_BREAK_EVENT: case CTRL_CLOSE_EVENT: case CTRL_LOGOFF_EVENT: case CTRL_SHUTDOWN_EVENT: self->message_loop_->PostTask(FROM_HERE, MessageLoop::QuitClosure()); self->stopped_event_.Wait(); return TRUE; default: return FALSE; } } HostService* HostService::GetInstance() { return Singleton::get(); } bool HostService::InitWithCommandLine(const CommandLine* command_line) { CommandLine::StringVector args = command_line->GetArgs(); // Choose the action to perform. bool host_binary_required = true; if (!args.empty()) { if (args.size() > 1) { LOG(ERROR) << "Invalid command line: more than one action requested."; return false; } if (args[0] == ASCIIToUTF16(kInstallActionName)) { run_routine_ = &HostService::Install; } else if (args[0] == ASCIIToUTF16(kRemoveActionName)) { run_routine_ = &HostService::Remove; host_binary_required = false; } else if (args[0] != ASCIIToUTF16(kRunActionName)) { LOG(ERROR) << "Invalid command line: invalid action specified: " << args[0]; return false; } } if (host_binary_required) { if (command_line->HasSwitch(kHostBinarySwitchName)) { host_binary_ = command_line->GetSwitchValuePath(kHostBinarySwitchName); } else { LOG(ERROR) << "Invalid command line: --" << kHostBinarySwitchName << " is required."; return false; } } // Run interactively if needed. if (run_routine_ == &HostService::RunAsService && command_line->HasSwitch(kConsoleSwitchName)) { run_routine_ = &HostService::RunInConsole; } return true; } int HostService::Install() { ScopedScHandle scmanager( OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE)); if (!scmanager.IsValid()) { LOG_GETLASTERROR(ERROR) << "Failed to connect to the service control manager"; return kErrorExitCode; } FilePath exe; if (!PathService::Get(base::FILE_EXE, &exe)) { LOG(ERROR) << "Unable to retrieve the service binary path."; return kErrorExitCode; } string16 name = StringPrintf(ASCIIToUTF16(kMuiStringFormat).c_str(), exe.value().c_str(), IDS_DISPLAY_SERVICE_NAME); if (!file_util::AbsolutePath(&host_binary_) || !file_util::PathExists(host_binary_)) { LOG(ERROR) << "Invalid host binary name: " << host_binary_.value(); return kErrorExitCode; } string16 command_line = StringPrintf( ASCIIToUTF16(kServiceCommandLineFormat).c_str(), exe.value().c_str(), host_binary_.value().c_str()); ScopedScHandle service( CreateServiceW(scmanager, service_name_.c_str(), name.c_str(), SERVICE_QUERY_STATUS | SERVICE_CHANGE_CONFIG, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, command_line.c_str(), NULL, NULL, ASCIIToUTF16(kServiceDependencies).c_str(), NULL, NULL)); if (service.IsValid()) { // Set the service description if the service is freshly installed. string16 description = StringPrintf( ASCIIToUTF16(kMuiStringFormat).c_str(), exe.value().c_str(), IDS_SERVICE_DESCRIPTION); SERVICE_DESCRIPTIONW info; info.lpDescription = const_cast(description.c_str()); if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_DESCRIPTION, &info)) { LOG_GETLASTERROR(ERROR) << "Failed to set the service description"; return kErrorExitCode; } printf("The service has been installed successfully.\n"); return kSuccessExitCode; } else { if (GetLastError() == ERROR_SERVICE_EXISTS) { printf("The service is installed already.\n"); return kSuccessExitCode; } else { LOG_GETLASTERROR(ERROR) << "Failed to create the service"; return kErrorExitCode; } } } VOID CALLBACK HostService::OnServiceStopped(PVOID context) { SERVICE_NOTIFY* notify = reinterpret_cast(context); DCHECK(notify != NULL); DCHECK(notify->dwNotificationStatus == ERROR_SUCCESS); DCHECK(notify->dwNotificationTriggered == SERVICE_NOTIFY_STOPPED); DCHECK(notify->ServiceStatus.dwCurrentState == SERVICE_STOPPED); } int HostService::Remove() { ScopedScHandle scmanager(OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT)); if (!scmanager.IsValid()) { LOG_GETLASTERROR(ERROR) << "Failed to connect to the service control manager"; return kErrorExitCode; } ScopedScHandle service( OpenServiceW(scmanager, service_name_.c_str(), DELETE | SERVICE_STOP | SERVICE_QUERY_STATUS)); if (!service.IsValid()) { if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) { printf("The service is not installed.\n"); return kSuccessExitCode; } else { LOG_GETLASTERROR(ERROR) << "Failed to open the service"; return kErrorExitCode; } } // Register for the service status notifications. The notification is // going to be delivered even if the service is stopped already. SERVICE_NOTIFY notify; ZeroMemory(¬ify, sizeof(notify)); notify.dwVersion = SERVICE_NOTIFY_STATUS_CHANGE; notify.pfnNotifyCallback = &HostService::OnServiceStopped; notify.pContext = this; // The notification callback will be unregistered once the service handle // is closed. if (ERROR_SUCCESS != NotifyServiceStatusChange( service, SERVICE_NOTIFY_STOPPED, ¬ify)) { LOG_GETLASTERROR(ERROR) << "Failed to register for the service status notifications"; return kErrorExitCode; } // Ask SCM to stop the service and wait. SERVICE_STATUS service_status; if (ControlService(service, SERVICE_CONTROL_STOP, &service_status)) { printf("Stopping...\n"); } DWORD wait_result = SleepEx(kServiceStopTimeoutMs, TRUE); if (wait_result != WAIT_IO_COMPLETION) { LOG(ERROR) << "Failed to stop the service."; return kErrorExitCode; } // Try to delete the service now. if (!DeleteService(service)) { LOG_GETLASTERROR(ERROR) << "Failed to delete the service"; return kErrorExitCode; } printf("The service has been removed successfully.\n"); return kSuccessExitCode; } int HostService::Run() { return (this->*run_routine_)(); } void HostService::RunMessageLoop() { // Launch the I/O thread. base::Thread io_thread(kIoThreadName); base::Thread::Options io_thread_options(MessageLoop::TYPE_IO, 0); if (!io_thread.StartWithOptions(io_thread_options)) { shutting_down_ = true; stopped_event_.Signal(); return; } WtsSessionProcessLauncher launcher(this, host_binary_, &io_thread); // Run the service. message_loop_->Run(); // Clean up the observers by emulating detaching from the console. shutting_down_ = true; OnSessionChange(); // Release the control handler. stopped_event_.Signal(); } int HostService::RunAsService() { SERVICE_TABLE_ENTRYW dispatch_table[] = { { const_cast(service_name_.c_str()), &HostService::ServiceMain }, { NULL, NULL } }; if (!StartServiceCtrlDispatcherW(dispatch_table)) { LOG_GETLASTERROR(ERROR) << "Failed to connect to the service control manager"; return kErrorExitCode; } return kSuccessExitCode; } int HostService::RunInConsole() { MessageLoop message_loop(MessageLoop::TYPE_UI); // Allow other threads to post to our message loop. message_loop_ = &message_loop; int result = kErrorExitCode; // Subscribe to Ctrl-C and other console events. if (!SetConsoleCtrlHandler(&HostService::ConsoleControlHandler, TRUE)) { LOG_GETLASTERROR(ERROR) << "Failed to set console control handler"; return result; } // Create a window for receiving session change notifications. LPCWSTR atom = NULL; HWND window = NULL; HINSTANCE instance = GetModuleHandle(NULL); string16 window_class = ASCIIToUTF16(kSessionNotificationWindowClass); WNDCLASSEX wc = {0}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = base::win::WrappedWindowProc; wc.hInstance = instance; wc.lpszClassName = window_class.c_str(); atom = reinterpret_cast(RegisterClassExW(&wc)); if (atom == NULL) { LOG_GETLASTERROR(ERROR) << "Failed to register the window class '" << kSessionNotificationWindowClass << "'"; goto cleanup; } window = CreateWindowW(atom, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, instance, 0); if (window == NULL) { LOG_GETLASTERROR(ERROR) << "Failed to creat the session notificationwindow"; goto cleanup; } // Post a dummy session change notification to peek up the current console // session. message_loop.PostTask(FROM_HERE, base::Bind( &HostService::OnSessionChange, base::Unretained(this))); // Subscribe to session change notifications. if (WTSRegisterSessionNotification(window, NOTIFY_FOR_ALL_SESSIONS) != FALSE) { // Run the service. RunMessageLoop(); WTSUnRegisterSessionNotification(window); result = kSuccessExitCode; } cleanup: if (window != NULL) { DestroyWindow(window); } if (atom != 0) { UnregisterClass(atom, instance); } // Unsubscribe from console events. Ignore the exit code. There is nothing // we can do about it now and the program is about to exit anyway. Even if // it crashes nothing is going to be broken because of it. SetConsoleCtrlHandler(&HostService::ConsoleControlHandler, FALSE); message_loop_ = NULL; return result; } DWORD WINAPI HostService::ServiceControlHandler(DWORD control, DWORD event_type, LPVOID event_data, LPVOID context) { HostService* self = reinterpret_cast(context); switch (control) { case SERVICE_CONTROL_INTERROGATE: return NO_ERROR; case SERVICE_CONTROL_SHUTDOWN: case SERVICE_CONTROL_STOP: self->message_loop_->PostTask(FROM_HERE, MessageLoop::QuitClosure()); self->stopped_event_.Wait(); return NO_ERROR; case SERVICE_CONTROL_SESSIONCHANGE: self->message_loop_->PostTask(FROM_HERE, base::Bind( &HostService::OnSessionChange, base::Unretained(self))); return NO_ERROR; default: return ERROR_CALL_NOT_IMPLEMENTED; } } VOID WINAPI HostService::ServiceMain(DWORD argc, WCHAR* argv[]) { MessageLoop message_loop; // Allow other threads to post to our message loop. HostService* self = HostService::GetInstance(); self->message_loop_ = &message_loop; // Register the service control handler. self->service_status_handle_ = RegisterServiceCtrlHandlerExW(self->service_name_.c_str(), &HostService::ServiceControlHandler, self); if (self->service_status_handle_ == 0) { LOG_GETLASTERROR(ERROR) << "Failed to register the service control handler"; return; } // Report running status of the service. SERVICE_STATUS service_status; ZeroMemory(&service_status, sizeof(service_status)); service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; service_status.dwCurrentState = SERVICE_RUNNING; service_status.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SESSIONCHANGE; service_status.dwWin32ExitCode = kSuccessExitCode; if (!SetServiceStatus(self->service_status_handle_, &service_status)) { LOG_GETLASTERROR(ERROR) << "Failed to report service status to the service control manager"; return; } // Post a dummy session change notification to peek up the current console // session. message_loop.PostTask(FROM_HERE, base::Bind( &HostService::OnSessionChange, base::Unretained(self))); // Run the service. self->RunMessageLoop(); // Tell SCM that the service is stopped. service_status.dwCurrentState = SERVICE_STOPPED; service_status.dwControlsAccepted = 0; if (!SetServiceStatus(self->service_status_handle_, &service_status)) { LOG_GETLASTERROR(ERROR) << "Failed to report service status to the service control manager"; return; } self->message_loop_ = NULL; } LRESULT CALLBACK HostService::SessionChangeNotificationProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { switch (message) { case WM_WTSSESSION_CHANGE: { HostService* self = HostService::GetInstance(); self->OnSessionChange(); return 0; } default: return DefWindowProc(hwnd, message, wparam, lparam); } } } // namespace remoting int main(int argc, char** argv) { CommandLine::Init(argc, argv); // This object instance is required by Chrome code (for example, // FilePath, LazyInstance, MessageLoop). base::AtExitManager exit_manager; // Write logs to the application profile directory. FilePath debug_log = remoting::GetConfigDir(). Append(FILE_PATH_LITERAL("debug.log")); InitLogging(debug_log.value().c_str(), logging::LOG_ONLY_TO_FILE, logging::DONT_LOCK_LOG_FILE, logging::APPEND_TO_OLD_LOG_FILE, logging::DISABLE_DCHECK_FOR_NON_OFFICIAL_RELEASE_BUILDS); const CommandLine* command_line = CommandLine::ForCurrentProcess(); if (command_line->HasSwitch(kHelpSwitchName) || command_line->HasSwitch(kQuestionSwitchName)) { usage(argv[0]); return kSuccessExitCode; } remoting::HostService* service = remoting::HostService::GetInstance(); if (!service->InitWithCommandLine(command_line)) { usage(argv[0]); return kUsageExitCode; } return service->Run(); }