Home | History | Annotate | Download | only in host
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "remoting/host/desktop_session_win.h"
      6 
      7 #include <limits>
      8 #include <sddl.h>
      9 
     10 #include "base/base_switches.h"
     11 #include "base/command_line.h"
     12 #include "base/files/file_path.h"
     13 #include "base/guid.h"
     14 #include "base/memory/ref_counted.h"
     15 #include "base/memory/scoped_ptr.h"
     16 #include "base/memory/weak_ptr.h"
     17 #include "base/path_service.h"
     18 #include "base/strings/stringprintf.h"
     19 #include "base/strings/utf_string_conversions.h"
     20 #include "base/threading/thread_checker.h"
     21 #include "base/timer/timer.h"
     22 #include "base/win/scoped_bstr.h"
     23 #include "base/win/scoped_comptr.h"
     24 #include "base/win/scoped_handle.h"
     25 #include "base/win/windows_version.h"
     26 #include "ipc/ipc_message_macros.h"
     27 #include "ipc/ipc_platform_file.h"
     28 #include "remoting/base/auto_thread_task_runner.h"
     29 // MIDL-generated declarations and definitions.
     30 #include "remoting/host/chromoting_lib.h"
     31 #include "remoting/host/chromoting_messages.h"
     32 #include "remoting/host/daemon_process.h"
     33 #include "remoting/host/desktop_session.h"
     34 #include "remoting/host/host_main.h"
     35 #include "remoting/host/ipc_constants.h"
     36 #include "remoting/host/sas_injector.h"
     37 #include "remoting/host/screen_resolution.h"
     38 #include "remoting/host/win/host_service.h"
     39 #include "remoting/host/win/worker_process_launcher.h"
     40 #include "remoting/host/win/wts_session_process_delegate.h"
     41 #include "remoting/host/win/wts_terminal_monitor.h"
     42 #include "remoting/host/win/wts_terminal_observer.h"
     43 #include "remoting/host/worker_process_ipc_delegate.h"
     44 
     45 using base::win::ScopedHandle;
     46 
     47 namespace remoting {
     48 
     49 namespace {
     50 
     51 // The security descriptor of the daemon IPC endpoint. It gives full access
     52 // to SYSTEM and denies access by anyone else.
     53 const wchar_t kDaemonIpcSecurityDescriptor[] =
     54     SDDL_OWNER L":" SDDL_LOCAL_SYSTEM
     55     SDDL_GROUP L":" SDDL_LOCAL_SYSTEM
     56     SDDL_DACL L":("
     57         SDDL_ACCESS_ALLOWED L";;" SDDL_GENERIC_ALL L";;;" SDDL_LOCAL_SYSTEM
     58     L")";
     59 
     60 // The command line parameters that should be copied from the service's command
     61 // line to the desktop process.
     62 const char* kCopiedSwitchNames[] = { switches::kV, switches::kVModule };
     63 
     64 // The default screen dimensions for an RDP session.
     65 const int kDefaultRdpScreenWidth = 1280;
     66 const int kDefaultRdpScreenHeight = 768;
     67 
     68 // RDC 6.1 (W2K8) supports dimensions of up to 4096x2048.
     69 const int kMaxRdpScreenWidth = 4096;
     70 const int kMaxRdpScreenHeight = 2048;
     71 
     72 // The minimum effective screen dimensions supported by Windows are 800x600.
     73 const int kMinRdpScreenWidth = 800;
     74 const int kMinRdpScreenHeight = 600;
     75 
     76 // Default dots per inch used by RDP is 96 DPI.
     77 const int kDefaultRdpDpi = 96;
     78 
     79 // The session attach notification should arrive within 30 seconds.
     80 const int kSessionAttachTimeoutSeconds = 30;
     81 
     82 // DesktopSession implementation which attaches to the host's physical console.
     83 // Receives IPC messages from the desktop process, running in the console
     84 // session, via |WorkerProcessIpcDelegate|, and monitors console session
     85 // attach/detach events via |WtsConsoleObserer|.
     86 class ConsoleSession : public DesktopSessionWin {
     87  public:
     88   // Same as DesktopSessionWin().
     89   ConsoleSession(
     90     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
     91     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
     92     DaemonProcess* daemon_process,
     93     int id,
     94     WtsTerminalMonitor* monitor);
     95   virtual ~ConsoleSession();
     96 
     97  protected:
     98   // DesktopSession overrides.
     99   virtual void SetScreenResolution(const ScreenResolution& resolution) OVERRIDE;
    100 
    101   // DesktopSessionWin overrides.
    102   virtual void InjectSas() OVERRIDE;
    103 
    104  private:
    105   scoped_ptr<SasInjector> sas_injector_;
    106 
    107   DISALLOW_COPY_AND_ASSIGN(ConsoleSession);
    108 };
    109 
    110 // DesktopSession implementation which attaches to virtual RDP console.
    111 // Receives IPC messages from the desktop process, running in the console
    112 // session, via |WorkerProcessIpcDelegate|, and monitors console session
    113 // attach/detach events via |WtsConsoleObserer|.
    114 class RdpSession : public DesktopSessionWin {
    115  public:
    116   // Same as DesktopSessionWin().
    117   RdpSession(
    118     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    119     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    120     DaemonProcess* daemon_process,
    121     int id,
    122     WtsTerminalMonitor* monitor);
    123   virtual ~RdpSession();
    124 
    125   // Performs the part of initialization that can fail.
    126   bool Initialize(const ScreenResolution& resolution);
    127 
    128   // Mirrors IRdpDesktopSessionEventHandler.
    129   void OnRdpConnected();
    130   void OnRdpClosed();
    131 
    132  protected:
    133   // DesktopSession overrides.
    134   virtual void SetScreenResolution(const ScreenResolution& resolution) OVERRIDE;
    135 
    136   // DesktopSessionWin overrides.
    137   virtual void InjectSas() OVERRIDE;
    138 
    139  private:
    140   // An implementation of IRdpDesktopSessionEventHandler interface that forwards
    141   // notifications to the owning desktop session.
    142   class EventHandler : public IRdpDesktopSessionEventHandler {
    143    public:
    144     explicit EventHandler(base::WeakPtr<RdpSession> desktop_session);
    145     virtual ~EventHandler();
    146 
    147     // IUnknown interface.
    148     STDMETHOD_(ULONG, AddRef)() OVERRIDE;
    149     STDMETHOD_(ULONG, Release)() OVERRIDE;
    150     STDMETHOD(QueryInterface)(REFIID riid, void** ppv) OVERRIDE;
    151 
    152     // IRdpDesktopSessionEventHandler interface.
    153     STDMETHOD(OnRdpConnected)() OVERRIDE;
    154     STDMETHOD(OnRdpClosed)() OVERRIDE;
    155 
    156    private:
    157     ULONG ref_count_;
    158 
    159     // Points to the desktop session object receiving OnRdpXxx() notifications.
    160     base::WeakPtr<RdpSession> desktop_session_;
    161 
    162     // This class must be used on a single thread.
    163     base::ThreadChecker thread_checker_;
    164 
    165     DISALLOW_COPY_AND_ASSIGN(EventHandler);
    166   };
    167 
    168   // Used to create an RDP desktop session.
    169   base::win::ScopedComPtr<IRdpDesktopSession> rdp_desktop_session_;
    170 
    171   // Used to match |rdp_desktop_session_| with the session it is attached to.
    172   std::string terminal_id_;
    173 
    174   base::WeakPtrFactory<RdpSession> weak_factory_;
    175 
    176   DISALLOW_COPY_AND_ASSIGN(RdpSession);
    177 };
    178 
    179 ConsoleSession::ConsoleSession(
    180     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    181     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    182     DaemonProcess* daemon_process,
    183     int id,
    184     WtsTerminalMonitor* monitor)
    185     : DesktopSessionWin(caller_task_runner, io_task_runner, daemon_process, id,
    186                         monitor) {
    187   StartMonitoring(WtsTerminalMonitor::kConsole);
    188 }
    189 
    190 ConsoleSession::~ConsoleSession() {
    191 }
    192 
    193 void ConsoleSession::SetScreenResolution(const ScreenResolution& resolution) {
    194   // Do nothing. The screen resolution of the console session is controlled by
    195   // the DesktopSessionAgent instance running in that session.
    196   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    197 }
    198 
    199 void ConsoleSession::InjectSas() {
    200   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    201 
    202   if (!sas_injector_)
    203     sas_injector_ = SasInjector::Create();
    204   if (!sas_injector_->InjectSas())
    205     LOG(ERROR) << "Failed to inject Secure Attention Sequence.";
    206 }
    207 
    208 RdpSession::RdpSession(
    209     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    210     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    211     DaemonProcess* daemon_process,
    212     int id,
    213     WtsTerminalMonitor* monitor)
    214     : DesktopSessionWin(caller_task_runner, io_task_runner, daemon_process, id,
    215                         monitor),
    216       weak_factory_(this) {
    217 }
    218 
    219 RdpSession::~RdpSession() {
    220 }
    221 
    222 bool RdpSession::Initialize(const ScreenResolution& resolution) {
    223   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    224 
    225   // Create the RDP wrapper object.
    226   HRESULT result = rdp_desktop_session_.CreateInstance(
    227       __uuidof(RdpDesktopSession));
    228   if (FAILED(result)) {
    229     LOG(ERROR) << "Failed to create RdpSession object, 0x"
    230                << std::hex << result << std::dec << ".";
    231     return false;
    232   }
    233 
    234   ScreenResolution local_resolution = resolution;
    235 
    236   // If the screen resolution is not specified, use the default screen
    237   // resolution.
    238   if (local_resolution.IsEmpty()) {
    239     local_resolution = ScreenResolution(
    240         webrtc::DesktopSize(kDefaultRdpScreenWidth, kDefaultRdpScreenHeight),
    241         webrtc::DesktopVector(kDefaultRdpDpi, kDefaultRdpDpi));
    242   }
    243 
    244   // Get the screen dimensions assuming the default DPI.
    245   webrtc::DesktopSize host_size = local_resolution.ScaleDimensionsToDpi(
    246       webrtc::DesktopVector(kDefaultRdpDpi, kDefaultRdpDpi));
    247 
    248   // Make sure that the host resolution is within the limits supported by RDP.
    249   host_size = webrtc::DesktopSize(
    250       std::min(kMaxRdpScreenWidth,
    251                std::max(kMinRdpScreenWidth, host_size.width())),
    252       std::min(kMaxRdpScreenHeight,
    253                std::max(kMinRdpScreenHeight, host_size.height())));
    254 
    255   // Create an RDP session.
    256   base::win::ScopedComPtr<IRdpDesktopSessionEventHandler> event_handler(
    257       new EventHandler(weak_factory_.GetWeakPtr()));
    258   terminal_id_ = base::GenerateGUID();
    259   base::win::ScopedBstr terminal_id(base::UTF8ToUTF16(terminal_id_).c_str());
    260   result = rdp_desktop_session_->Connect(host_size.width(),
    261                                          host_size.height(),
    262                                          terminal_id,
    263                                          event_handler);
    264   if (FAILED(result)) {
    265     LOG(ERROR) << "RdpSession::Create() failed, 0x"
    266                << std::hex << result << std::dec << ".";
    267     return false;
    268   }
    269 
    270   return true;
    271 }
    272 
    273 void RdpSession::OnRdpConnected() {
    274   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    275 
    276   StopMonitoring();
    277   StartMonitoring(terminal_id_);
    278 }
    279 
    280 void RdpSession::OnRdpClosed() {
    281   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    282 
    283   TerminateSession();
    284 }
    285 
    286 void RdpSession::SetScreenResolution(const ScreenResolution& resolution) {
    287   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    288 
    289   // TODO(alexeypa): implement resize-to-client for RDP sessions here.
    290   // See http://crbug.com/137696.
    291   NOTIMPLEMENTED();
    292 }
    293 
    294 void RdpSession::InjectSas() {
    295   DCHECK(caller_task_runner()->BelongsToCurrentThread());
    296 
    297   rdp_desktop_session_->InjectSas();
    298 }
    299 
    300 RdpSession::EventHandler::EventHandler(
    301     base::WeakPtr<RdpSession> desktop_session)
    302     : ref_count_(0),
    303       desktop_session_(desktop_session) {
    304 }
    305 
    306 RdpSession::EventHandler::~EventHandler() {
    307   DCHECK(thread_checker_.CalledOnValidThread());
    308 
    309   if (desktop_session_)
    310     desktop_session_->OnRdpClosed();
    311 }
    312 
    313 ULONG STDMETHODCALLTYPE RdpSession::EventHandler::AddRef() {
    314   DCHECK(thread_checker_.CalledOnValidThread());
    315 
    316   return ++ref_count_;
    317 }
    318 
    319 ULONG STDMETHODCALLTYPE RdpSession::EventHandler::Release() {
    320   DCHECK(thread_checker_.CalledOnValidThread());
    321 
    322   if (--ref_count_ == 0) {
    323     delete this;
    324     return 0;
    325   }
    326 
    327   return ref_count_;
    328 }
    329 
    330 STDMETHODIMP RdpSession::EventHandler::QueryInterface(REFIID riid, void** ppv) {
    331   DCHECK(thread_checker_.CalledOnValidThread());
    332 
    333   if (riid == IID_IUnknown ||
    334       riid == IID_IRdpDesktopSessionEventHandler) {
    335     *ppv = static_cast<IRdpDesktopSessionEventHandler*>(this);
    336     AddRef();
    337     return S_OK;
    338   }
    339 
    340   *ppv = NULL;
    341   return E_NOINTERFACE;
    342 }
    343 
    344 STDMETHODIMP RdpSession::EventHandler::OnRdpConnected() {
    345   DCHECK(thread_checker_.CalledOnValidThread());
    346 
    347   if (desktop_session_)
    348     desktop_session_->OnRdpConnected();
    349 
    350   return S_OK;
    351 }
    352 
    353 STDMETHODIMP RdpSession::EventHandler::OnRdpClosed() {
    354   DCHECK(thread_checker_.CalledOnValidThread());
    355 
    356   if (!desktop_session_)
    357     return S_OK;
    358 
    359   base::WeakPtr<RdpSession> desktop_session = desktop_session_;
    360   desktop_session_.reset();
    361   desktop_session->OnRdpClosed();
    362   return S_OK;
    363 }
    364 
    365 } // namespace
    366 
    367 // static
    368 scoped_ptr<DesktopSession> DesktopSessionWin::CreateForConsole(
    369     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    370     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    371     DaemonProcess* daemon_process,
    372     int id,
    373     const ScreenResolution& resolution) {
    374   scoped_ptr<ConsoleSession> session(new ConsoleSession(
    375       caller_task_runner, io_task_runner, daemon_process, id,
    376       HostService::GetInstance()));
    377 
    378   return session.PassAs<DesktopSession>();
    379 }
    380 
    381 // static
    382 scoped_ptr<DesktopSession> DesktopSessionWin::CreateForVirtualTerminal(
    383     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    384     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    385     DaemonProcess* daemon_process,
    386     int id,
    387     const ScreenResolution& resolution) {
    388   scoped_ptr<RdpSession> session(new RdpSession(
    389       caller_task_runner, io_task_runner, daemon_process, id,
    390       HostService::GetInstance()));
    391   if (!session->Initialize(resolution))
    392     return scoped_ptr<DesktopSession>();
    393 
    394   return session.PassAs<DesktopSession>();
    395 }
    396 
    397 DesktopSessionWin::DesktopSessionWin(
    398     scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
    399     scoped_refptr<AutoThreadTaskRunner> io_task_runner,
    400     DaemonProcess* daemon_process,
    401     int id,
    402     WtsTerminalMonitor* monitor)
    403     : DesktopSession(daemon_process, id),
    404       caller_task_runner_(caller_task_runner),
    405       io_task_runner_(io_task_runner),
    406       monitor_(monitor),
    407       monitoring_notifications_(false) {
    408   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    409 
    410   ReportElapsedTime("created");
    411 }
    412 
    413 DesktopSessionWin::~DesktopSessionWin() {
    414   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    415 
    416   StopMonitoring();
    417 }
    418 
    419 void DesktopSessionWin::OnSessionAttachTimeout() {
    420   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    421 
    422   LOG(ERROR) << "Session attach notification didn't arrived within "
    423              << kSessionAttachTimeoutSeconds << " seconds.";
    424   TerminateSession();
    425 }
    426 
    427 void DesktopSessionWin::StartMonitoring(const std::string& terminal_id) {
    428   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    429   DCHECK(!monitoring_notifications_);
    430   DCHECK(!session_attach_timer_.IsRunning());
    431 
    432   ReportElapsedTime("started monitoring");
    433 
    434   session_attach_timer_.Start(
    435       FROM_HERE, base::TimeDelta::FromSeconds(kSessionAttachTimeoutSeconds),
    436       this, &DesktopSessionWin::OnSessionAttachTimeout);
    437 
    438   monitoring_notifications_ = true;
    439   monitor_->AddWtsTerminalObserver(terminal_id, this);
    440 }
    441 
    442 void DesktopSessionWin::StopMonitoring() {
    443   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    444 
    445   if (monitoring_notifications_) {
    446     ReportElapsedTime("stopped monitoring");
    447 
    448     monitoring_notifications_ = false;
    449     monitor_->RemoveWtsTerminalObserver(this);
    450   }
    451 
    452   session_attach_timer_.Stop();
    453   OnSessionDetached();
    454 }
    455 
    456 void DesktopSessionWin::TerminateSession() {
    457   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    458 
    459   StopMonitoring();
    460 
    461   // This call will delete |this| so it should be at the very end of the method.
    462   daemon_process()->CloseDesktopSession(id());
    463 }
    464 
    465 void DesktopSessionWin::OnChannelConnected(int32 peer_pid) {
    466   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    467 
    468   ReportElapsedTime("channel connected");
    469 
    470   // Obtain the handle of the desktop process. It will be passed to the network
    471   // process to use to duplicate handles of shared memory objects from
    472   // the desktop process.
    473   desktop_process_.Set(OpenProcess(PROCESS_DUP_HANDLE, false, peer_pid));
    474   if (!desktop_process_.IsValid()) {
    475     CrashDesktopProcess(FROM_HERE);
    476     return;
    477   }
    478 
    479   VLOG(1) << "IPC: daemon <- desktop (" << peer_pid << ")";
    480 }
    481 
    482 bool DesktopSessionWin::OnMessageReceived(const IPC::Message& message) {
    483   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    484 
    485   bool handled = true;
    486   IPC_BEGIN_MESSAGE_MAP(DesktopSessionWin, message)
    487     IPC_MESSAGE_HANDLER(ChromotingDesktopDaemonMsg_DesktopAttached,
    488                         OnDesktopSessionAgentAttached)
    489     IPC_MESSAGE_HANDLER(ChromotingDesktopDaemonMsg_InjectSas,
    490                         InjectSas)
    491     IPC_MESSAGE_UNHANDLED(handled = false)
    492   IPC_END_MESSAGE_MAP()
    493 
    494   if (!handled) {
    495     LOG(ERROR) << "Received unexpected IPC type: " << message.type();
    496     CrashDesktopProcess(FROM_HERE);
    497   }
    498 
    499   return handled;
    500 }
    501 
    502 void DesktopSessionWin::OnPermanentError(int exit_code) {
    503   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    504 
    505   TerminateSession();
    506 }
    507 
    508 void DesktopSessionWin::OnSessionAttached(uint32 session_id) {
    509   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    510   DCHECK(!launcher_);
    511   DCHECK(monitoring_notifications_);
    512 
    513   ReportElapsedTime("attached");
    514 
    515   // Launch elevated on Win8 to be able to inject Alt+Tab.
    516   bool launch_elevated = base::win::GetVersion() >= base::win::VERSION_WIN8;
    517 
    518   // Get the name of the executable to run. |kDesktopBinaryName| specifies
    519   // uiAccess="true" in it's manifest.
    520   base::FilePath desktop_binary;
    521   bool result;
    522   if (launch_elevated) {
    523     result = GetInstalledBinaryPath(kDesktopBinaryName, &desktop_binary);
    524   } else {
    525     result = GetInstalledBinaryPath(kHostBinaryName, &desktop_binary);
    526   }
    527 
    528   if (!result) {
    529     TerminateSession();
    530     return;
    531   }
    532 
    533   session_attach_timer_.Stop();
    534 
    535   scoped_ptr<CommandLine> target(new CommandLine(desktop_binary));
    536   target->AppendSwitchASCII(kProcessTypeSwitchName, kProcessTypeDesktop);
    537   // Copy the command line switches enabling verbose logging.
    538   target->CopySwitchesFrom(*CommandLine::ForCurrentProcess(),
    539                            kCopiedSwitchNames,
    540                            arraysize(kCopiedSwitchNames));
    541 
    542   // Create a delegate capable of launching a process in a different session.
    543   scoped_ptr<WtsSessionProcessDelegate> delegate(
    544       new WtsSessionProcessDelegate(io_task_runner_,
    545                                     target.Pass(),
    546                                     launch_elevated,
    547                                     base::WideToUTF8(
    548                                         kDaemonIpcSecurityDescriptor)));
    549   if (!delegate->Initialize(session_id)) {
    550     TerminateSession();
    551     return;
    552   }
    553 
    554   // Create a launcher for the desktop process, using the per-session delegate.
    555   launcher_.reset(new WorkerProcessLauncher(
    556       delegate.PassAs<WorkerProcessLauncher::Delegate>(), this));
    557 }
    558 
    559 void DesktopSessionWin::OnSessionDetached() {
    560   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    561 
    562   launcher_.reset();
    563 
    564   if (monitoring_notifications_) {
    565     ReportElapsedTime("detached");
    566 
    567     session_attach_timer_.Start(
    568         FROM_HERE, base::TimeDelta::FromSeconds(kSessionAttachTimeoutSeconds),
    569         this, &DesktopSessionWin::OnSessionAttachTimeout);
    570   }
    571 }
    572 
    573 void DesktopSessionWin::OnDesktopSessionAgentAttached(
    574       IPC::PlatformFileForTransit desktop_pipe) {
    575   if (!daemon_process()->OnDesktopSessionAgentAttached(id(),
    576                                                        desktop_process_.Get(),
    577                                                        desktop_pipe)) {
    578     CrashDesktopProcess(FROM_HERE);
    579   }
    580 }
    581 
    582 void DesktopSessionWin::CrashDesktopProcess(
    583     const tracked_objects::Location& location) {
    584   DCHECK(caller_task_runner_->BelongsToCurrentThread());
    585 
    586   launcher_->Crash(location);
    587 }
    588 
    589 void DesktopSessionWin::ReportElapsedTime(const std::string& event) {
    590   base::Time now = base::Time::Now();
    591 
    592   std::string passed;
    593   if (!last_timestamp_.is_null()) {
    594     passed = base::StringPrintf(", %.2fs passed",
    595                                 (now - last_timestamp_).InSecondsF());
    596   }
    597 
    598   base::Time::Exploded exploded;
    599   now.LocalExplode(&exploded);
    600   VLOG(1) << base::StringPrintf("session(%d): %s at %02d:%02d:%02d.%03d%s",
    601                                 id(),
    602                                 event.c_str(),
    603                                 exploded.hour,
    604                                 exploded.minute,
    605                                 exploded.second,
    606                                 exploded.millisecond,
    607                                 passed.c_str());
    608 
    609   last_timestamp_ = now;
    610 }
    611 
    612 }  // namespace remoting
    613