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/curtain_mode.h"
6
7#include <ApplicationServices/ApplicationServices.h>
8#include <Carbon/Carbon.h>
9#include <Security/Security.h>
10#include <unistd.h>
11
12#include "base/bind.h"
13#include "base/location.h"
14#include "base/logging.h"
15#include "base/mac/mac_util.h"
16#include "base/mac/scoped_cftyperef.h"
17#include "base/single_thread_task_runner.h"
18#include "remoting/host/client_session_control.h"
19
20namespace {
21
22using remoting::ClientSessionControl;
23
24const char* kCGSessionPath =
25    "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/"
26    "CGSession";
27
28// Used to detach the current session from the local console and disconnect
29// the connnection if it gets re-attached.
30//
31// Because the switch-in handler can only called on the main (UI) thread, this
32// class installs the handler and detaches the current session from the console
33// on the UI thread as well.
34class SessionWatcher : public base::RefCountedThreadSafe<SessionWatcher> {
35 public:
36  SessionWatcher(
37      scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
38      scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
39      base::WeakPtr<ClientSessionControl> client_session_control);
40
41  void Start();
42  void Stop();
43
44 private:
45  friend class base::RefCountedThreadSafe<SessionWatcher>;
46  virtual ~SessionWatcher();
47
48  // Detaches the session from the console and install the switch-in handler to
49  // detect when the session re-attaches back.
50  void ActivateCurtain();
51
52  // Installs the switch-in handler.
53  bool InstallEventHandler();
54
55  // Removes the switch-in handler.
56  void RemoveEventHandler();
57
58  // Disconnects the client session.
59  void DisconnectSession();
60
61  // Handlers for the switch-in event.
62  static OSStatus SessionActivateHandler(EventHandlerCallRef handler,
63                                         EventRef event,
64                                         void* user_data);
65
66  // Task runner on which public methods of this class must be called.
67  scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner_;
68
69  // Task runner representing the thread receiving Carbon events.
70  scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner_;
71
72  // Used to disconnect the client session.
73  base::WeakPtr<ClientSessionControl> client_session_control_;
74
75  EventHandlerRef event_handler_;
76
77  DISALLOW_COPY_AND_ASSIGN(SessionWatcher);
78};
79
80SessionWatcher::SessionWatcher(
81    scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
82    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
83    base::WeakPtr<ClientSessionControl> client_session_control)
84    : caller_task_runner_(caller_task_runner),
85      ui_task_runner_(ui_task_runner),
86      client_session_control_(client_session_control),
87      event_handler_(NULL) {
88}
89
90void SessionWatcher::Start() {
91  DCHECK(caller_task_runner_->BelongsToCurrentThread());
92
93  // Activate curtain asynchronously since it has to be done on the UI thread.
94  // Because the curtain activation is asynchronous, it is possible that
95  // the connection will not be curtained for a brief moment. This seems to be
96  // unaviodable as long as the curtain enforcement depends on processing of
97  // the switch-in notifications.
98  ui_task_runner_->PostTask(
99      FROM_HERE, base::Bind(&SessionWatcher::ActivateCurtain, this));
100}
101
102void SessionWatcher::Stop() {
103  DCHECK(caller_task_runner_->BelongsToCurrentThread());
104
105  client_session_control_.reset();
106  ui_task_runner_->PostTask(
107      FROM_HERE, base::Bind(&SessionWatcher::RemoveEventHandler, this));
108}
109
110SessionWatcher::~SessionWatcher() {
111  DCHECK(!event_handler_);
112}
113
114void SessionWatcher::ActivateCurtain() {
115  // Curtain mode causes problems with the login screen on Lion only (starting
116  // with 10.7.3), so disable it on that platform. There is a work-around, but
117  // it involves modifying a system Plist pertaining to power-management, so
118  // it's not something that should be done automatically. For more details,
119  // see https://discussions.apple.com/thread/3209415?start=690&tstart=0
120  //
121  // TODO(jamiewalch): If the underlying OS bug is ever fixed, we should support
122  // curtain mode on suitable versions of Lion.
123  if (base::mac::IsOSLion()) {
124    LOG(ERROR) << "Host curtaining is not supported on Mac OS X 10.7.";
125    DisconnectSession();
126    return;
127  }
128
129  // Try to install the switch-in handler. Do this before switching out the
130  // current session so that the console session is not affected if it fails.
131  if (!InstallEventHandler()) {
132    LOG(ERROR) << "Failed to install the switch-in handler.";
133    DisconnectSession();
134    return;
135  }
136
137  base::ScopedCFTypeRef<CFDictionaryRef> session(
138      CGSessionCopyCurrentDictionary());
139
140  // CGSessionCopyCurrentDictionary has been observed to return NULL in some
141  // cases. Once the system is in this state, curtain mode will fail as the
142  // CGSession command thinks the session is not attached to the console. The
143  // only known remedy is logout or reboot. Since we're not sure what causes
144  // this, or how common it is, a crash report is useful in this case (note
145  // that the connection would have to be refused in any case, so this is no
146  // loss of functionality).
147  CHECK(session != NULL);
148
149  const void* on_console = CFDictionaryGetValue(session,
150                                                kCGSessionOnConsoleKey);
151  const void* logged_in = CFDictionaryGetValue(session, kCGSessionLoginDoneKey);
152  if (logged_in == kCFBooleanTrue && on_console == kCFBooleanTrue) {
153    pid_t child = fork();
154    if (child == 0) {
155      execl(kCGSessionPath, kCGSessionPath, "-suspend", NULL);
156      _exit(1);
157    } else if (child > 0) {
158      int status = 0;
159      waitpid(child, &status, 0);
160      if (status != 0) {
161        LOG(ERROR) << kCGSessionPath << " failed.";
162        DisconnectSession();
163        return;
164      }
165    } else {
166      LOG(ERROR) << "fork() failed.";
167      DisconnectSession();
168      return;
169    }
170  }
171}
172
173bool SessionWatcher::InstallEventHandler() {
174  DCHECK(ui_task_runner_->BelongsToCurrentThread());
175  DCHECK(!event_handler_);
176
177  EventTypeSpec event;
178  event.eventClass = kEventClassSystem;
179  event.eventKind = kEventSystemUserSessionActivated;
180  OSStatus result = ::InstallApplicationEventHandler(
181      NewEventHandlerUPP(SessionActivateHandler), 1, &event, this,
182      &event_handler_);
183  if (result != noErr) {
184    event_handler_ = NULL;
185    DisconnectSession();
186    return false;
187  }
188
189  return true;
190}
191
192void SessionWatcher::RemoveEventHandler() {
193  DCHECK(ui_task_runner_->BelongsToCurrentThread());
194
195  if (event_handler_) {
196    ::RemoveEventHandler(event_handler_);
197    event_handler_ = NULL;
198  }
199}
200
201void SessionWatcher::DisconnectSession() {
202  if (!caller_task_runner_->BelongsToCurrentThread()) {
203    caller_task_runner_->PostTask(
204        FROM_HERE, base::Bind(&SessionWatcher::DisconnectSession, this));
205    return;
206  }
207
208  if (client_session_control_)
209    client_session_control_->DisconnectSession();
210}
211
212OSStatus SessionWatcher::SessionActivateHandler(EventHandlerCallRef handler,
213                                                EventRef event,
214                                                void* user_data) {
215  static_cast<SessionWatcher*>(user_data)->DisconnectSession();
216  return noErr;
217}
218
219}  // namespace
220
221namespace remoting {
222
223class CurtainModeMac : public CurtainMode {
224 public:
225  CurtainModeMac(
226      scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
227      scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
228      base::WeakPtr<ClientSessionControl> client_session_control);
229  virtual ~CurtainModeMac();
230
231  // Overriden from CurtainMode.
232  virtual bool Activate() OVERRIDE;
233
234 private:
235  scoped_refptr<SessionWatcher> session_watcher_;
236
237  DISALLOW_COPY_AND_ASSIGN(CurtainModeMac);
238};
239
240CurtainModeMac::CurtainModeMac(
241    scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
242    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
243    base::WeakPtr<ClientSessionControl> client_session_control)
244    : session_watcher_(new SessionWatcher(caller_task_runner,
245                                          ui_task_runner,
246                                          client_session_control)) {
247}
248
249CurtainModeMac::~CurtainModeMac() {
250  session_watcher_->Stop();
251}
252
253bool CurtainModeMac::Activate() {
254  session_watcher_->Start();
255  return true;
256}
257
258// static
259scoped_ptr<CurtainMode> CurtainMode::Create(
260    scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
261    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
262    base::WeakPtr<ClientSessionControl> client_session_control) {
263  return scoped_ptr<CurtainMode>(new CurtainModeMac(caller_task_runner,
264                                                    ui_task_runner,
265                                                    client_session_control));
266}
267
268}  // namespace remoting
269