diff options
-rw-r--r-- | base/message_loop.cc | 4 | ||||
-rw-r--r-- | base/message_loop.h | 3 | ||||
-rw-r--r-- | base/message_pump_win.cc | 51 | ||||
-rw-r--r-- | base/message_pump_win.h | 19 | ||||
-rw-r--r-- | remoting/base/stoppable.cc | 2 | ||||
-rw-r--r-- | remoting/host/host_user_interface.cc | 3 | ||||
-rw-r--r-- | remoting/host/win/host_service.cc | 101 | ||||
-rw-r--r-- | remoting/host/win/host_service.h | 3 | ||||
-rw-r--r-- | remoting/host/win/launch_process_with_token.cc | 12 | ||||
-rw-r--r-- | remoting/host/win/launch_process_with_token.h | 4 | ||||
-rw-r--r-- | remoting/host/win/wts_session_process_launcher.cc | 352 | ||||
-rw-r--r-- | remoting/host/win/wts_session_process_launcher.h | 41 | ||||
-rw-r--r-- | remoting/remoting.gyp | 3 |
13 files changed, 464 insertions, 134 deletions
diff --git a/base/message_loop.cc b/base/message_loop.cc index 7cadede..32994bf 100644 --- a/base/message_loop.cc +++ b/base/message_loop.cc @@ -751,6 +751,10 @@ void MessageLoopForIO::RegisterIOHandler(HANDLE file, IOHandler* handler) { pump_io()->RegisterIOHandler(file, handler); } +bool MessageLoopForIO::RegisterJobObject(HANDLE job, IOHandler* handler) { + return pump_io()->RegisterJobObject(job, handler); +} + bool MessageLoopForIO::WaitForIOCompletion(DWORD timeout, IOHandler* filter) { return pump_io()->WaitForIOCompletion(timeout, filter); } diff --git a/base/message_loop.h b/base/message_loop.h index 2ff963f..b54ca85 100644 --- a/base/message_loop.h +++ b/base/message_loop.h @@ -631,7 +631,8 @@ class BASE_EXPORT MessageLoopForIO : public MessageLoop { #if defined(OS_WIN) // Please see MessagePumpWin for definitions of these methods. - void RegisterIOHandler(HANDLE file_handle, IOHandler* handler); + void RegisterIOHandler(HANDLE file, IOHandler* handler); + bool RegisterJobObject(HANDLE job, IOHandler* handler); bool WaitForIOCompletion(DWORD timeout, IOHandler* filter); protected: diff --git a/base/message_pump_win.cc b/base/message_pump_win.cc index 2b2a10e..40118da 100644 --- a/base/message_pump_win.cc +++ b/base/message_pump_win.cc @@ -475,11 +475,26 @@ void MessagePumpForIO::ScheduleDelayedWork(const TimeTicks& delayed_work_time) { void MessagePumpForIO::RegisterIOHandler(HANDLE file_handle, IOHandler* handler) { - ULONG_PTR key = reinterpret_cast<ULONG_PTR>(handler); + ULONG_PTR key = HandlerToKey(handler, true); HANDLE port = CreateIoCompletionPort(file_handle, port_, key, 1); DPCHECK(port); } +bool MessagePumpForIO::RegisterJobObject(HANDLE job_handle, + IOHandler* handler) { + // Job object notifications use the OVERLAPPED pointer to carry the message + // data. Mark the completion key correspondingly, so we will not try to + // convert OVERLAPPED* to IOContext*. + ULONG_PTR key = HandlerToKey(handler, false); + JOBOBJECT_ASSOCIATE_COMPLETION_PORT info; + info.CompletionKey = reinterpret_cast<void*>(key); + info.CompletionPort = port_; + return SetInformationJobObject(job_handle, + JobObjectAssociateCompletionPortInformation, + &info, + sizeof(info)) != FALSE; +} + //----------------------------------------------------------------------------- // MessagePumpForIO private: @@ -546,12 +561,16 @@ bool MessagePumpForIO::WaitForIOCompletion(DWORD timeout, IOHandler* filter) { return true; } - if (item.context->handler) { + // If |item.has_valid_io_context| is false then |item.context| does not point + // to a context structure, and so should not be dereferenced, although it may + // still hold valid non-pointer data. + if (!item.has_valid_io_context || item.context->handler) { if (filter && item.handler != filter) { // Save this item for later completed_io_.push_back(item); } else { - DCHECK_EQ(item.context->handler, item.handler); + DCHECK(!item.has_valid_io_context || + (item.context->handler == item.handler)); WillProcessIOEvent(); item.handler->OnIOCompleted(item.context, item.bytes_transfered, item.error); @@ -577,7 +596,7 @@ bool MessagePumpForIO::GetIOItem(DWORD timeout, IOItem* item) { item->bytes_transfered = 0; } - item->handler = reinterpret_cast<IOHandler*>(key); + item->handler = KeyToHandler(key, &item->has_valid_io_context); item->context = reinterpret_cast<IOContext*>(overlapped); return true; } @@ -623,4 +642,28 @@ void MessagePumpForIO::DidProcessIOEvent() { FOR_EACH_OBSERVER(IOObserver, io_observers_, DidProcessIOEvent()); } +// static +ULONG_PTR MessagePumpForIO::HandlerToKey(IOHandler* handler, + bool has_valid_io_context) { + ULONG_PTR key = reinterpret_cast<ULONG_PTR>(handler); + + // |IOHandler| is at least pointer-size aligned, so the lowest two bits are + // always cleared. We use the lowest bit to distinguish completion keys with + // and without the associated |IOContext|. + DCHECK((key & 1) == 0); + + // Mark the completion key as context-less. + if (!has_valid_io_context) + key = key | 1; + return key; +} + +// static +MessagePumpForIO::IOHandler* MessagePumpForIO::KeyToHandler( + ULONG_PTR key, + bool* has_valid_io_context) { + *has_valid_io_context = ((key & 1) == 0); + return reinterpret_cast<IOHandler*>(key & ~static_cast<ULONG_PTR>(1)); +} + } // namespace base diff --git a/base/message_pump_win.h b/base/message_pump_win.h index e778e01..fd46198 100644 --- a/base/message_pump_win.h +++ b/base/message_pump_win.h @@ -296,6 +296,12 @@ class BASE_EXPORT MessagePumpForIO : public MessagePumpWin { // |handler| must be valid as long as there is pending IO for the given file. void RegisterIOHandler(HANDLE file_handle, IOHandler* handler); + // Register the handler to be used to process job events. The registration + // persists as long as the job object is live, so |handler| must be valid + // until the job object is destroyed. Returns true if the registration + // succeeded, and false otherwise. + bool RegisterJobObject(HANDLE job_handle, IOHandler* handler); + // Waits for the next IO completion that should be processed by |filter|, for // up to |timeout| milliseconds. Return true if any IO operation completed, // regardless of the involved handler, and false if the timeout expired. If @@ -316,6 +322,11 @@ class BASE_EXPORT MessagePumpForIO : public MessagePumpWin { IOContext* context; DWORD bytes_transfered; DWORD error; + + // In some cases |context| can be a non-pointer value casted to a pointer. + // |has_valid_io_context| is true if |context| is a valid IOContext + // pointer, and false otherwise. + bool has_valid_io_context; }; virtual void DoRunLoop(); @@ -326,6 +337,14 @@ class BASE_EXPORT MessagePumpForIO : public MessagePumpWin { void WillProcessIOEvent(); void DidProcessIOEvent(); + // Converts an IOHandler pointer to a completion port key. + // |has_valid_io_context| specifies whether completion packets posted to + // |handler| will have valid OVERLAPPED pointers. + static ULONG_PTR HandlerToKey(IOHandler* handler, bool has_valid_io_context); + + // Converts a completion port key to an IOHandler pointer. + static IOHandler* KeyToHandler(ULONG_PTR key, bool* has_valid_io_context); + // The completion port associated with this thread. win::ScopedHandle port_; // This list will be empty almost always. It stores IO completions that have diff --git a/remoting/base/stoppable.cc b/remoting/base/stoppable.cc index 046ac42..99acba4 100644 --- a/remoting/base/stoppable.cc +++ b/remoting/base/stoppable.cc @@ -18,7 +18,7 @@ Stoppable::Stoppable( } Stoppable::~Stoppable() { - DCHECK_EQ(state_, kStopped); + CHECK_EQ(state_, kStopped); } void Stoppable::Stop() { diff --git a/remoting/host/host_user_interface.cc b/remoting/host/host_user_interface.cc index 925e106..9172370 100644 --- a/remoting/host/host_user_interface.cc +++ b/remoting/host/host_user_interface.cc @@ -32,7 +32,6 @@ void HostUserInterface::Start(ChromotingHost* host, const base::Closure& disconnect_callback) { DCHECK(network_task_runner()->BelongsToCurrentThread()); DCHECK(host_ == NULL); - DCHECK(disconnect_callback_.is_null()); host_ = host; disconnect_callback_ = disconnect_callback; @@ -91,7 +90,6 @@ base::SingleThreadTaskRunner* HostUserInterface::ui_task_runner() const { void HostUserInterface::DisconnectSession() const { DCHECK(ui_task_runner()->BelongsToCurrentThread()); - DCHECK(!disconnect_callback_.is_null()); disconnect_callback_.Run(); } @@ -118,7 +116,6 @@ void HostUserInterface::StartForTest( scoped_ptr<LocalInputMonitor> local_input_monitor) { DCHECK(network_task_runner()->BelongsToCurrentThread()); DCHECK(host_ == NULL); - DCHECK(disconnect_callback_.is_null()); host_ = host; disconnect_callback_ = disconnect_callback; diff --git a/remoting/host/win/host_service.cc b/remoting/host/win/host_service.cc index 0c6ccb5..145c930 100644 --- a/remoting/host/win/host_service.cc +++ b/remoting/host/win/host_service.cc @@ -8,11 +8,12 @@ #include "remoting/host/win/host_service.h" #include <windows.h> +#include <shellapi.h> #include <wtsapi32.h> -#include <stdio.h> #include "base/at_exit.h" #include "base/base_paths.h" +#include "base/base_switches.h" #include "base/bind.h" #include "base/command_line.h" #include "base/file_path.h" @@ -53,35 +54,42 @@ const char kIoThreadName[] = "I/O thread"; const wchar_t kSessionNotificationWindowClass[] = L"Chromoting_SessionNotificationWindow"; -// Command line actions and switches: -// "run" sumply runs the service as usual. -const wchar_t kRunActionName[] = L"run"; +// Command line switches: // "--console" runs the service interactively for debugging purposes. const char kConsoleSwitchName[] = "console"; +// "--elevate=<binary>" requests <binary> to be launched elevated, presenting +// a UAC prompt if necessary. +const char kElevateSwitchName[] = "elevate"; + // "--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" - "\n" - "Options:\n" - " --console - Run the service interactively for debugging purposes.\n" - " --help, --? - Print this message.\n"; +const wchar_t kUsageMessage[] = + L"\n" + L"Usage: %ls [options]\n" + L"\n" + L"Options:\n" + L" --console - Run the service interactively for debugging purposes.\n" + L" --elevate=<...> - Run <...> elevated.\n" + L" --help, --? - Print this message.\n"; + +// The command line parameters that should be copied from the service's command +// line when launching an elevated child. +const char* kCopiedSwitchNames[] = { + "auth-config", "host-config", "chromoting-ipc", switches::kV, + switches::kVModule }; // 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); +void usage(const FilePath& program_name) { + LOG(INFO) << StringPrintf(kUsageMessage, + UTF16ToWide(program_name.value()).c_str()); } } // namespace @@ -164,17 +172,15 @@ HostService* HostService::GetInstance() { bool HostService::InitWithCommandLine(const CommandLine* command_line) { CommandLine::StringVector args = command_line->GetArgs(); - // Choose the action to perform. + // Check if launch with elevation was requested. + if (command_line->HasSwitch(kElevateSwitchName)) { + run_routine_ = &HostService::Elevate; + return true; + } + if (!args.empty()) { - if (args.size() > 1) { - LOG(ERROR) << "Invalid command line: more than one action requested."; - return false; - } - if (args[0] != kRunActionName) { - LOG(ERROR) << "Invalid command line: invalid action specified: " - << args[0]; - return false; - } + LOG(ERROR) << "No positional parameters expected."; + return false; } // Run interactively if needed. @@ -225,6 +231,35 @@ void HostService::RunMessageLoop(MessageLoop* message_loop) { stopped_event_.Signal(); } +int HostService::Elevate() { + // Get the name of the binary to launch. + FilePath binary = + CommandLine::ForCurrentProcess()->GetSwitchValuePath(kElevateSwitchName); + + // Create the child process command line by copying known switches from our + // command line. + CommandLine command_line(CommandLine::NO_PROGRAM); + command_line.CopySwitchesFrom(*CommandLine::ForCurrentProcess(), + kCopiedSwitchNames, + _countof(kCopiedSwitchNames)); + CommandLine::StringType parameters = command_line.GetCommandLineString(); + + // Launch the child process requesting elevation. + SHELLEXECUTEINFO info; + memset(&info, 0, sizeof(info)); + info.cbSize = sizeof(info); + info.lpVerb = L"runas"; + info.lpFile = binary.value().c_str(); + info.lpParameters = parameters.c_str(); + info.nShow = SW_SHOWNORMAL; + + if (!ShellExecuteEx(&info)) { + return GetLastError(); + } + + return kSuccessExitCode; +} + int HostService::RunAsService() { SERVICE_TABLE_ENTRYW dispatch_table[] = { { const_cast<LPWSTR>(kWindowsServiceName), &HostService::ServiceMain }, @@ -409,14 +444,19 @@ LRESULT CALLBACK HostService::SessionChangeNotificationProc(HWND hwnd, } // namespace remoting -int main(int argc, char** argv) { +int CALLBACK WinMain(HINSTANCE instance, + HINSTANCE previous_instance, + LPSTR raw_command_line, + int show_command) { #ifdef OFFICIAL_BUILD if (remoting::IsUsageStatsAllowed()) { remoting::InitializeCrashReporting(); } #endif // OFFICIAL_BUILD - CommandLine::Init(argc, argv); + // CommandLine::Init() ignores the passed |argc| and |argv| on Windows getting + // the command line from GetCommandLineW(), so we can safely pass NULL here. + CommandLine::Init(0, NULL); // This object instance is required by Chrome code (for example, // FilePath, LazyInstance, MessageLoop). @@ -432,16 +472,15 @@ int main(int argc, char** argv) { 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]); + usage(command_line->GetProgram()); return kSuccessExitCode; } remoting::HostService* service = remoting::HostService::GetInstance(); if (!service->InitWithCommandLine(command_line)) { - usage(argv[0]); + usage(command_line->GetProgram()); return kUsageExitCode; } diff --git a/remoting/host/win/host_service.h b/remoting/host/win/host_service.h index 5ff5a79..9533f02 100644 --- a/remoting/host/win/host_service.h +++ b/remoting/host/win/host_service.h @@ -61,6 +61,9 @@ class HostService : public WtsConsoleMonitor { // RunAsService() and RunInConsole(). void RunMessageLoop(MessageLoop* message_loop); + // Runs the binary specified by the command line, elevated. + int Elevate(); + // This function handshakes with the service control manager and starts // the service. int RunAsService(); diff --git a/remoting/host/win/launch_process_with_token.cc b/remoting/host/win/launch_process_with_token.cc index ae0e1e6..1320e2a 100644 --- a/remoting/host/win/launch_process_with_token.cc +++ b/remoting/host/win/launch_process_with_token.cc @@ -103,6 +103,7 @@ bool CreateRemoteSessionProcess( uint32 session_id, const FilePath::StringType& application_name, const CommandLine::StringType& command_line, + DWORD creation_flags, PROCESS_INFORMATION* process_information_out) { DCHECK(base::win::GetVersion() == base::win::VERSION_XP); @@ -203,8 +204,9 @@ bool CreateRemoteSessionProcess( CreateProcessRequest* request = reinterpret_cast<CreateProcessRequest*>(buffer.get()); request->size = size; - request->use_default_token = TRUE; request->process_id = GetCurrentProcessId(); + request->use_default_token = TRUE; + request->creation_flags = creation_flags; request->startup_info.cb = sizeof(request->startup_info); size_t buffer_offset = sizeof(CreateProcessRequest); @@ -345,7 +347,9 @@ bool CreateSessionToken(uint32 session_id, ScopedHandle* token_out) { bool LaunchProcessWithToken(const FilePath& binary, const CommandLine::StringType& command_line, HANDLE user_token, - ScopedHandle* process_out) { + DWORD creation_flags, + ScopedHandle* process_out, + ScopedHandle* thread_out) { FilePath::StringType application_name = binary.value(); base::win::ScopedProcessInformation process_info; @@ -363,7 +367,7 @@ bool LaunchProcessWithToken(const FilePath& binary, NULL, NULL, FALSE, - 0, + creation_flags, NULL, NULL, &startup_info, @@ -389,6 +393,7 @@ bool LaunchProcessWithToken(const FilePath& binary, result = CreateRemoteSessionProcess(session_id, application_name, command_line, + creation_flags, process_info.Receive()); } else { // Restore the error status returned by CreateProcessAsUser(). @@ -405,6 +410,7 @@ bool LaunchProcessWithToken(const FilePath& binary, CHECK(process_info.IsValid()); process_out->Set(process_info.TakeProcessHandle()); + thread_out->Set(process_info.TakeThreadHandle()); return true; } diff --git a/remoting/host/win/launch_process_with_token.h b/remoting/host/win/launch_process_with_token.h index 196fdeb..8f46219 100644 --- a/remoting/host/win/launch_process_with_token.h +++ b/remoting/host/win/launch_process_with_token.h @@ -23,7 +23,9 @@ bool CreateSessionToken(uint32 session_id, base::win::ScopedHandle* token_out); bool LaunchProcessWithToken(const FilePath& binary, const CommandLine::StringType& command_line, HANDLE user_token, - base::win::ScopedHandle* process_out); + DWORD creation_flags, + base::win::ScopedHandle* process_out, + base::win::ScopedHandle* thread_out); } // namespace remoting diff --git a/remoting/host/win/wts_session_process_launcher.cc b/remoting/host/win/wts_session_process_launcher.cc index 42cb702..bec5db1 100644 --- a/remoting/host/win/wts_session_process_launcher.cc +++ b/remoting/host/win/wts_session_process_launcher.cc @@ -47,9 +47,14 @@ const int kMinLaunchDelaySeconds = 1; const FilePath::CharType kMe2meHostBinaryName[] = FILE_PATH_LITERAL("remoting_me2me_host.exe"); +const FilePath::CharType kMe2meServiceBinaryName[] = + FILE_PATH_LITERAL("remoting_service.exe"); + // The IPC channel name is passed to the host in the command line. const char kChromotingIpcSwitchName[] = "chromoting-ipc"; +const char kElevateSwitchName[] = "elevate"; + // The command line parameters that should be copied from the service's command // line to the host process. const char* kCopiedSwitchNames[] = { @@ -72,87 +77,57 @@ WtsSessionProcessLauncher::WtsSessionProcessLauncher( attached_(false), main_message_loop_(main_message_loop), ipc_message_loop_(ipc_message_loop), - monitor_(monitor) { + monitor_(monitor), + job_state_(kJobUninitialized) { monitor_->AddWtsConsoleObserver(this); -} -WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { - monitor_->RemoveWtsConsoleObserver(this); - - DCHECK(!attached_); - DCHECK(!timer_.IsRunning()); -} - -void WtsSessionProcessLauncher::LaunchProcess() { - DCHECK(main_message_loop_->BelongsToCurrentThread()); - DCHECK(attached_); - DCHECK(launcher_.get() == NULL); - DCHECK(!timer_.IsRunning()); - DCHECK(!worker_process_.IsValid()); + process_exit_event_.Set(CreateEvent(NULL, TRUE, FALSE, NULL)); + CHECK(process_exit_event_.IsValid()); - launch_time_ = base::Time::Now(); - launcher_.reset(new WorkerProcessLauncher( - this, - base::Bind(&WtsSessionProcessLauncher::OnLauncherStopped, - base::Unretained(this)), - main_message_loop_, - ipc_message_loop_)); - launcher_->Start(kChromotingChannelSecurityDescriptor); + // To receive job object notifications it is registered with the completion + // port represented by |ipc_message_loop|. The registration has to be done on + // the I/O thread because MessageLoopForIO::RegisterJobObject() can only be + // called via MessageLoopForIO::current(). + ipc_message_loop_->PostTask(FROM_HERE, base::Bind( + &WtsSessionProcessLauncher::InitializeJob, + base::Unretained(this))); } -void WtsSessionProcessLauncher::OnLauncherStopped() { - DCHECK(main_message_loop_->BelongsToCurrentThread()); - - DWORD exit_code; - if (!::GetExitCodeProcess(worker_process_, &exit_code)) { - LOG_GETLASTERROR(INFO) - << "Failed to query the exit code of the worker process"; - exit_code = CONTROL_C_EXIT; - } - - launcher_.reset(NULL); - worker_process_.Close(); - - // Do not relaunch the worker process if the caller has asked us to stop. - if (stoppable_state() != Stoppable::kRunning) { - CompleteStopping(); - return; - } +WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { + // Make sure that the object is completely stopped. The same check exists + // in Stoppable::~Stoppable() but this one allows us to examine the state of + // the object before destruction. + CHECK_EQ(stoppable_state(), Stoppable::kStopped); - // Stop trying to restart the worker process if its process exited due to - // misconfiguration. - if (kMinPermanentErrorExitCode <= exit_code && - exit_code <= kMaxPermanentErrorExitCode) { - Stop(); - return; - } + monitor_->RemoveWtsConsoleObserver(this); - // Try to restart the worker process if we are still attached to a session. - if (attached_) { - // Expand the backoff interval if the process has died quickly or reset it - // if it was up longer than the maximum backoff delay. - base::TimeDelta delta = base::Time::Now() - launch_time_; - if (delta < base::TimeDelta() || - delta >= base::TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)) { - launch_backoff_ = base::TimeDelta(); - } else { - launch_backoff_ = std::max( - launch_backoff_ * 2, TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); - launch_backoff_ = std::min( - launch_backoff_, TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); - } + CHECK(!attached_); + CHECK(!timer_.IsRunning()); +} - // Try to launch the worker process. - timer_.Start(FROM_HERE, launch_backoff_, - this, &WtsSessionProcessLauncher::LaunchProcess); - } +void WtsSessionProcessLauncher::OnIOCompleted( + base::MessagePumpForIO::IOContext* context, + DWORD bytes_transferred, + DWORD error) { + DCHECK(ipc_message_loop_->BelongsToCurrentThread()); + + // |bytes_transferred| is used in job object notifications to supply + // the message ID; |context| carries process ID. + main_message_loop_->PostTask(FROM_HERE, base::Bind( + &WtsSessionProcessLauncher::OnJobNotification, + base::Unretained(this), bytes_transferred, + reinterpret_cast<DWORD>(context))); } bool WtsSessionProcessLauncher::DoLaunchProcess( const std::string& channel_name, ScopedHandle* process_exit_event_out) { DCHECK(main_message_loop_->BelongsToCurrentThread()); - DCHECK(!worker_process_.IsValid()); + + // The job object is not ready. Retry starting the host process later. + if (!job_.IsValid()) { + return false; + } // Construct the host binary name. FilePath dir_path; @@ -161,28 +136,48 @@ bool WtsSessionProcessLauncher::DoLaunchProcess( return false; } FilePath host_binary = dir_path.Append(kMe2meHostBinaryName); + FilePath service_binary = dir_path.Append(kMe2meServiceBinaryName); // Create the host process command line passing the name of the IPC channel // to use and copying known switches from the service's command line. - CommandLine command_line(host_binary); + CommandLine command_line(service_binary); + command_line.AppendSwitchPath(kElevateSwitchName, host_binary); command_line.AppendSwitchNative(kChromotingIpcSwitchName, UTF8ToWide(channel_name)); command_line.CopySwitchesFrom(*CommandLine::ForCurrentProcess(), kCopiedSwitchNames, _countof(kCopiedSwitchNames)); + CHECK(ResetEvent(process_exit_event_)); + // Try to launch the process and attach an object watcher to the returned // handle so that we get notified when the process terminates. - if (!LaunchProcessWithToken(host_binary, + base::win::ScopedHandle worker_process; + base::win::ScopedHandle worker_thread; + if (!LaunchProcessWithToken(service_binary, command_line.GetCommandLineString(), session_token_, - &worker_process_)) { + CREATE_SUSPENDED, + &worker_process, + &worker_thread)) { + return false; + } + + if (!AssignProcessToJobObject(job_, worker_process)) { + LOG_GETLASTERROR(ERROR) << "Failed to assign the worker to the job object"; + TerminateProcess(worker_process, CONTROL_C_EXIT); + return false; + } + + if (!ResumeThread(worker_thread)) { + LOG_GETLASTERROR(ERROR) << "Failed to resume the worker thread"; + DoKillProcess(CONTROL_C_EXIT); return false; } ScopedHandle process_exit_event; if (!DuplicateHandle(GetCurrentProcess(), - worker_process_, + process_exit_event_, GetCurrentProcess(), process_exit_event.Receive(), SYNCHRONIZE, @@ -200,8 +195,8 @@ bool WtsSessionProcessLauncher::DoLaunchProcess( void WtsSessionProcessLauncher::DoKillProcess(DWORD exit_code) { DCHECK(main_message_loop_->BelongsToCurrentThread()); - if (worker_process_.IsValid()) { - TerminateProcess(worker_process_, exit_code); + if (job_.IsValid()) { + TerminateJobObject(job_, exit_code); } } @@ -221,20 +216,6 @@ bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) { return handled; } -void WtsSessionProcessLauncher::OnSendSasToConsole() { - DCHECK(main_message_loop_->BelongsToCurrentThread()); - - if (attached_) { - if (sas_injector_.get() == NULL) { - sas_injector_ = SasInjector::Create(); - } - - if (sas_injector_.get() != NULL) { - sas_injector_->InjectSas(); - } - } -} - void WtsSessionProcessLauncher::OnSessionAttached(uint32 session_id) { DCHECK(main_message_loop_->BelongsToCurrentThread()); @@ -276,8 +257,201 @@ void WtsSessionProcessLauncher::DoStop() { OnSessionDetached(); } - if (launcher_.get() == NULL) { - CompleteStopping(); + job_.Close(); + + // Drain the completion queue to make sure all job object notification have + // been received. + if (job_state_ == kJobRunning) { + job_state_ = kJobStopping; + ipc_message_loop_->PostTask(FROM_HERE, base::Bind( + &WtsSessionProcessLauncher::DrainJobNotifications, + base::Unretained(this))); + } + + // Don't complete shutdown if |launcher_| is not completely stopped. + if (launcher_.get() != NULL) { + return; + } + + // Don't complete shutdown if the completion queue hasn't been drained. + if (job_state_ != kJobUninitialized && job_state_ != kJobStopped) { + return; + } + + CompleteStopping(); +} + +void WtsSessionProcessLauncher::DrainJobNotifications() { + DCHECK(ipc_message_loop_->BelongsToCurrentThread()); + + // DrainJobNotifications() is posted after the job object is destroyed, so + // by this time all notifications from the job object have been processed + // already. Let the main thread know that the queue has been drained. + main_message_loop_->PostTask(FROM_HERE, base::Bind( + &WtsSessionProcessLauncher::DrainJobNotificationsCompleted, + base::Unretained(this))); +} + +void WtsSessionProcessLauncher::DrainJobNotificationsCompleted() { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + DCHECK_EQ(job_state_, kJobStopping); + + job_state_ = kJobStopped; + Stop(); +} + +void WtsSessionProcessLauncher::InitializeJob() { + DCHECK(ipc_message_loop_->BelongsToCurrentThread()); + + ScopedHandle job; + job.Set(CreateJobObject(NULL, NULL)); + if (!job.IsValid()) { + LOG_GETLASTERROR(ERROR) << "Failed to create a job object"; + return; + } + + // Limit the number of active processes in the job to two (the process + // performing elevation and the host) and make sure that all processes will be + // killed once the job object is destroyed. + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info; + memset(&info, 0, sizeof(info)); + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS | + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + info.BasicLimitInformation.ActiveProcessLimit = 2; + if (!SetInformationJobObject(job, + JobObjectExtendedLimitInformation, + &info, + sizeof(info))) { + LOG_GETLASTERROR(ERROR) << "Failed to set limits on the job object"; + return; + } + + // Register the job object with the completion port in the I/O thread to + // receive job notifications. + if (!MessageLoopForIO::current()->RegisterJobObject(job, this)) { + LOG_GETLASTERROR(ERROR) + << "Failed to associate the job object with a completion port"; + return; + } + + // ScopedHandle is not compatible with base::Passed, so we wrap it to a scoped + // pointer. + scoped_ptr<ScopedHandle> job_wrapper(new ScopedHandle()); + *job_wrapper = job.Pass(); + + // Let the main thread know that initialization is complete. + main_message_loop_->PostTask(FROM_HERE, base::Bind( + &WtsSessionProcessLauncher::InitializeJobCompleted, + base::Unretained(this), base::Passed(&job_wrapper))); +} + +void WtsSessionProcessLauncher::InitializeJobCompleted( + scoped_ptr<ScopedHandle> job) { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + DCHECK(!job_.IsValid()); + DCHECK_EQ(job_state_, kJobUninitialized); + + if (stoppable_state() == Stoppable::kRunning) { + job_ = job->Pass(); + job_state_ = kJobRunning; + } +} + +void WtsSessionProcessLauncher::OnJobNotification(DWORD message, DWORD pid) { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + + switch (message) { + case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: + CHECK(SetEvent(process_exit_event_)); + break; + + case JOB_OBJECT_MSG_NEW_PROCESS: + // We report the exit code of the worker process to be |CONTROL_C_EXIT| + // if we cannot get the actual exit code. So here we can safely ignore + // the error returned by OpenProcess(). + worker_process_.Set(OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid)); + break; + } +} + +void WtsSessionProcessLauncher::LaunchProcess() { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + DCHECK(attached_); + DCHECK(launcher_.get() == NULL); + DCHECK(!timer_.IsRunning()); + + launch_time_ = base::Time::Now(); + launcher_.reset(new WorkerProcessLauncher( + this, + base::Bind(&WtsSessionProcessLauncher::OnLauncherStopped, + base::Unretained(this)), + main_message_loop_, + ipc_message_loop_)); + launcher_->Start(kChromotingChannelSecurityDescriptor); +} + +void WtsSessionProcessLauncher::OnLauncherStopped() { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + + DWORD exit_code = CONTROL_C_EXIT; + if (worker_process_.IsValid()) { + if (!::GetExitCodeProcess(worker_process_, &exit_code)) { + LOG_GETLASTERROR(INFO) + << "Failed to query the exit code of the worker process"; + exit_code = CONTROL_C_EXIT; + } + + worker_process_.Close(); + } + + launcher_.reset(NULL); + + // Do not relaunch the worker process if the caller has asked us to stop. + if (stoppable_state() != Stoppable::kRunning) { + Stop(); + return; + } + + // Stop trying to restart the worker process if its process exited due to + // misconfiguration. + if (kMinPermanentErrorExitCode <= exit_code && + exit_code <= kMaxPermanentErrorExitCode) { + Stop(); + return; + } + + // Try to restart the worker process if we are still attached to a session. + if (attached_) { + // Expand the backoff interval if the process has died quickly or reset it + // if it was up longer than the maximum backoff delay. + base::TimeDelta delta = base::Time::Now() - launch_time_; + if (delta < base::TimeDelta() || + delta >= base::TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)) { + launch_backoff_ = base::TimeDelta(); + } else { + launch_backoff_ = std::max( + launch_backoff_ * 2, TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); + launch_backoff_ = std::min( + launch_backoff_, TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); + } + + // Try to launch the worker process. + timer_.Start(FROM_HERE, launch_backoff_, + this, &WtsSessionProcessLauncher::LaunchProcess); + } +} + +void WtsSessionProcessLauncher::OnSendSasToConsole() { + DCHECK(main_message_loop_->BelongsToCurrentThread()); + + if (attached_) { + if (sas_injector_.get() == NULL) { + sas_injector_ = SasInjector::Create(); + } + + if (sas_injector_.get() != NULL) { + sas_injector_->InjectSas(); + } } } diff --git a/remoting/host/win/wts_session_process_launcher.h b/remoting/host/win/wts_session_process_launcher.h index 9a91903..ce1a2f9 100644 --- a/remoting/host/win/wts_session_process_launcher.h +++ b/remoting/host/win/wts_session_process_launcher.h @@ -11,6 +11,7 @@ #include "base/compiler_specific.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" #include "base/process.h" #include "base/time.h" #include "base/timer.h" @@ -35,7 +36,8 @@ class SasInjector; class WtsConsoleMonitor; class WtsSessionProcessLauncher - : public Stoppable, + : public base::MessagePumpForIO::IOHandler, + public Stoppable, public WorkerProcessLauncher::Delegate, public WtsConsoleObserver { public: @@ -51,6 +53,11 @@ class WtsSessionProcessLauncher virtual ~WtsSessionProcessLauncher(); + // base::MessagePumpForIO::IOHandler implementation. + virtual void OnIOCompleted(base::MessagePumpForIO::IOContext* context, + DWORD bytes_transferred, + DWORD error) OVERRIDE; + // WorkerProcessLauncher::Delegate implementation. virtual bool DoLaunchProcess( const std::string& channel_name, @@ -68,6 +75,22 @@ class WtsSessionProcessLauncher virtual void DoStop() OVERRIDE; private: + // Drains the completion port queue to make sure that all job object + // notifications has been received. + void DrainJobNotifications(); + + // Notifies that the completion port queue has been drained. + void DrainJobNotificationsCompleted(); + + // Creates and initializes the job object that will sandbox the launched child + // processes. + void InitializeJob(); + + // Notifies that the job object initialization is complete. + void InitializeJobCompleted(scoped_ptr<base::win::ScopedHandle> job); + + void OnJobNotification(DWORD message, DWORD pid); + // Attempts to launch the host process in the current console session. // Schedules next launch attempt if creation of the process fails for any // reason. @@ -103,6 +126,22 @@ class WtsSessionProcessLauncher scoped_ptr<WorkerProcessLauncher> launcher_; + // The job object used to control the lifetime of child processes. + base::win::ScopedHandle job_; + + // A waiting handle that becomes signalled once all process associated with + // the job have been terminated. + base::win::ScopedHandle process_exit_event_; + + enum JobState { + kJobUninitialized, + kJobRunning, + kJobStopping, + kJobStopped + }; + + JobState job_state_; + base::win::ScopedHandle worker_process_; // The token to be used to launch a process in a different session. diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index a06ce228..8b0fbd4 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -632,6 +632,8 @@ 'AdditionalDependencies': [ 'wtsapi32.lib', ], + # 2 == /SUBSYSTEM:WINDOWS + 'SubSystem': '2', }, }, }, # end of target 'remoting_service' @@ -1518,6 +1520,7 @@ 'msvs_settings': { 'VCLinkerTool': { 'AdditionalOptions': [ + "\"/MANIFESTUAC:level='requireAdministrator' uiAccess='true'\"", "\"/manifestdependency:type='win32' " "name='Microsoft.Windows.Common-Controls' " "version='6.0.0.0' " |