OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2011 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 "chrome/common/multi_process_notification.h" | |
6 | |
7 #import <Foundation/Foundation.h> | |
8 #include <notify.h> | |
9 #include <sys/select.h> | |
10 #include <sys/socket.h> | |
11 #include <sys/types.h> | |
12 #include <unistd.h> | |
13 | |
14 #include <algorithm> | |
15 | |
16 #include "base/basictypes.h" | |
17 #include "base/eintr_wrapper.h" | |
18 #include "base/file_path.h" | |
19 #include "base/logging.h" | |
20 #include "base/mac/mac_util.h" | |
21 #include "base/mac/scoped_nsautorelease_pool.h" | |
22 #include "base/message_loop_proxy.h" | |
23 #include "base/message_pump_libevent.h" | |
24 #include "base/path_service.h" | |
25 #include "base/ref_counted.h" | |
26 #include "base/stringprintf.h" | |
27 #include "base/synchronization/lock.h" | |
28 #include "base/sys_string_conversions.h" | |
29 #include "base/sys_info.h" | |
30 #include "base/threading/simple_thread.h" | |
31 #include "chrome/browser/browser_thread.h" | |
32 #include "chrome/common/chrome_paths.h" | |
33 | |
34 // Enable this to build with leopard_switchboard_thread | |
35 #define USE_LEOPARD_SWITCHBOARD_THREAD 1 | |
Mark Mentovai
2011/01/13 23:10:34
Turn this back off.
dmac
2011/01/13 23:17:43
Done.
| |
36 | |
37 namespace { | |
38 | |
39 std::string AddPrefixToNotification(const std::string& name, | |
40 multi_process_notification::Domain domain) { | |
41 // The ordering of the components in the string returned by this function | |
42 // is important. Read "NAMESPACE CONVENTIONS" in 'man 3 notify' for details. | |
43 base::mac::ScopedNSAutoreleasePool pool; | |
44 NSBundle* bundle = base::mac::MainAppBundle(); | |
45 NSString* ns_bundle_id = [bundle bundleIdentifier]; | |
46 std::string bundle_id = base::SysNSStringToUTF8(ns_bundle_id); | |
47 std::string domain_string; | |
48 switch (domain) { | |
49 case multi_process_notification::ProfileDomain: { | |
50 FilePath user_data_dir; | |
51 if (!PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) { | |
52 NOTREACHED(); | |
53 } | |
54 domain_string = StringPrintf("user.uid.%u.%s.", | |
55 getuid(), user_data_dir.value().c_str()); | |
56 break; | |
57 } | |
58 | |
59 case multi_process_notification::UserDomain: | |
60 domain_string = StringPrintf("user.uid.%u.", getuid()); | |
61 break; | |
62 | |
63 case multi_process_notification::SystemDomain: | |
64 break; | |
65 } | |
66 return domain_string + bundle_id + "." + name; | |
67 } | |
68 | |
69 bool UseLeopardSwitchboardThread() { | |
70 #if USE_LEOPARD_SWITCHBOARD_THREAD | |
71 return true; | |
72 #endif // USE_LEOPARD_SWITCHBOARD_THREAD | |
73 int32 major_version, minor_version, bugfix_version; | |
74 base::SysInfo::OperatingSystemVersionNumbers( | |
75 &major_version, &minor_version, &bugfix_version); | |
76 return major_version < 10 || (major_version == 10 && minor_version <= 5); | |
77 } | |
78 | |
79 } // namespace | |
80 | |
81 namespace multi_process_notification { | |
82 | |
83 bool Post(const std::string& name, Domain domain) { | |
84 std::string notification = AddPrefixToNotification(name, domain); | |
85 uint32_t status = notify_post(notification.c_str()); | |
86 DCHECK_EQ(status, static_cast<uint32_t>(NOTIFY_STATUS_OK)); | |
87 return status == NOTIFY_STATUS_OK; | |
88 } | |
89 | |
90 #if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5 | |
91 #error LeopardSwitchboardThread can be removed | |
92 #endif // MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5 | |
93 | |
94 // LeopardSwitchboardThread exists because the file descriptors returned by | |
95 // notify_register_file_descriptor can't be monitored using kqueues on 10.5 | |
96 // ( http://openradar.appspot.com/8854692 ) and libevent uses kqueue to watch | |
97 // file descriptors in IOMessageLoop. | |
98 // This solution is to have a separate thread that monitors the file descriptor | |
99 // returned by notify_register_file_descriptor using select, and then to | |
100 // notify the MessageLoopForIO using a different file descriptor allocated by | |
101 // socketpair that can be monitored using kqueues in libevent. This thread | |
102 // only runs on 10.5, as 10.6 kqueues can monitor the notify file descriptors | |
103 // without any problems. | |
104 | |
105 // LeopardSwitchboardThread creates three file descriptors: | |
106 // internal_fd_: which communicates from the switchboard thread to other threads | |
107 // external_fd_: which communicates from other threads to the switchboard thread | |
108 // notify_fd_: which is the file descriptor returned from | |
109 // notify_register_file_descriptor | |
110 // | |
111 // The thread itself sits in a select loop waiting on internal_fd_, and | |
112 // notify_fd_ for input. If it gets ANY input on internal_fd_ it exits. | |
113 // If it gets input on notify_fd_ it sends the input through to external_fd_. | |
114 // External_fd_ is monitored by MessageLoopForIO so that the lookup of any | |
115 // matching listeners in entries_, and the triggering of those listeners, | |
116 // occurs in the MessageLoopForIO thread. | |
117 // | |
118 // Lookups are linear right now, and could be optimized if they ever become | |
119 // a performance issue. | |
120 class LeopardSwitchboardThread | |
121 : public base::MessagePumpLibevent::Watcher, | |
122 public base::SimpleThread, | |
123 public MessageLoop::DestructionObserver { | |
124 public: | |
125 LeopardSwitchboardThread(); | |
126 virtual ~LeopardSwitchboardThread(); | |
127 | |
128 bool Init(); | |
129 | |
130 bool AddListener(ListenerImpl* listener, | |
131 const std::string& notification); | |
132 bool RemoveListener(ListenerImpl* listener, const std::string& notification); | |
133 | |
134 bool finished() const { return finished_; } | |
135 | |
136 // SimpleThread overrides | |
137 virtual void Run(); | |
138 | |
139 // Watcher overrides | |
140 virtual void OnFileCanReadWithoutBlocking(int fd); | |
141 virtual void OnFileCanWriteWithoutBlocking(int fd); | |
142 | |
143 // DestructionObserver overrides | |
144 virtual void WillDestroyCurrentMessageLoop(); | |
145 | |
146 private: | |
147 // User to match tokens to notifications and vice-versa. | |
Mark Mentovai
2011/01/13 23:10:34
“User” should be “Used”.
dmac
2011/01/13 23:17:43
Done.
| |
148 struct SwitchboardEntry { | |
149 int token_; | |
150 std::string notification_; | |
151 ListenerImpl* listener_; | |
152 }; | |
153 | |
154 enum { | |
155 kKillThreadMessage = 0xdecea5e | |
156 }; | |
157 | |
158 int internal_fd_; | |
159 int external_fd_; | |
160 int notify_fd_; | |
161 int notify_fd_token_; | |
162 mutable bool finished_; | |
163 fd_set fd_set_; | |
164 | |
165 // all accesses to entries_ must be controlled by entries_lock_. | |
166 std::vector<SwitchboardEntry> entries_; | |
167 Lock entries_lock_; | |
168 base::MessagePumpLibevent::FileDescriptorWatcher watcher_; | |
169 }; | |
170 | |
171 class ListenerImpl : public base::MessagePumpLibevent::Watcher { | |
172 public: | |
173 ListenerImpl(const std::string& name, | |
174 Domain domain, | |
175 Listener::Delegate* delegate); | |
176 virtual ~ListenerImpl(); | |
177 | |
178 bool Start(); | |
179 void OnListen(); | |
180 | |
181 // Watcher overrides | |
182 virtual void OnFileCanReadWithoutBlocking(int fd); | |
183 virtual void OnFileCanWriteWithoutBlocking(int fd); | |
184 | |
185 private: | |
186 std::string name_; | |
187 Domain domain_; | |
188 Listener::Delegate* delegate_; | |
189 int fd_; | |
190 int token_; | |
191 Lock switchboard_lock_; | |
192 static LeopardSwitchboardThread* g_switchboard_thread_; | |
193 base::MessagePumpLibevent::FileDescriptorWatcher watcher_; | |
194 scoped_refptr<base::MessageLoopProxy> message_loop_proxy_; | |
195 | |
196 void StartLeopard(); | |
197 void StartSnowLeopard(); | |
198 DISALLOW_COPY_AND_ASSIGN(ListenerImpl); | |
199 }; | |
200 | |
201 | |
202 LeopardSwitchboardThread::LeopardSwitchboardThread() | |
203 : base::SimpleThread("LeopardSwitchboardThread"), internal_fd_(-1), | |
204 external_fd_(-1), notify_fd_(-1), notify_fd_token_(-1), finished_(false) { | |
205 } | |
206 | |
207 LeopardSwitchboardThread::~LeopardSwitchboardThread() { | |
208 if (internal_fd_ != -1) { | |
209 close(internal_fd_); | |
210 } | |
211 if (external_fd_ != -1) { | |
212 close(external_fd_); | |
213 } | |
214 if (notify_fd_token_ != -1) { | |
215 // Cancelling this notification takes care of closing notify_fd_. | |
216 uint32_t status = notify_cancel(notify_fd_token_); | |
217 DCHECK_EQ(status, static_cast<uint32_t>(NOTIFY_STATUS_OK)); | |
218 } | |
219 } | |
220 | |
221 bool LeopardSwitchboardThread::Init() { | |
222 // Create a pair of sockets for communicating with the thread | |
223 // The file descriptors returned from socketpair can be kqueue'd on 10.5. | |
224 int sockets[2]; | |
225 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) { | |
226 PLOG(ERROR) << "socketpair"; | |
227 return false; | |
228 } | |
229 internal_fd_ = sockets[0]; | |
230 external_fd_ = sockets[1]; | |
231 | |
232 // Register a bogus notification so that there is single notify_fd_ to | |
233 // monitor. This runs a small risk of overflowing the notification buffer | |
234 // if notifications are used heavily (see man 3 notify), however it greatly | |
235 // simplifies the select loop code as there are only 2 file descriptors | |
236 // that need to be monitored, and there is no need to add/remove file | |
237 // descriptors from fd_set_ as listeners are added and removed. | |
238 // This also keep the total fd usage on 10.5 to three for all | |
239 // notifications. The 10.6 implementation will use one fd per notification, | |
240 // but doesn't run the risk of notification buffer overflow. If fds ever | |
241 // become tight, the 10.6 code could be changed to use only one fd for | |
242 // all notifications. | |
243 std::string notification = StringPrintf("LeopardSwitchboardThread.%d", | |
244 getpid()); | |
245 notification = AddPrefixToNotification(notification, ProfileDomain); | |
246 uint32_t status = notify_register_file_descriptor( | |
247 notification.c_str(), ¬ify_fd_, 0, ¬ify_fd_token_); | |
248 if (status != NOTIFY_STATUS_OK) { | |
249 return false; | |
250 } | |
251 | |
252 FD_ZERO(&fd_set_); | |
253 FD_SET(internal_fd_, &fd_set_); | |
254 FD_SET(notify_fd_, &fd_set_); | |
255 | |
256 MessageLoopForIO* io_loop = MessageLoopForIO::current(); | |
257 // Watch for destruction of the BrowserThread::IO message loop so that | |
258 // the thread can be exited cleanly. | |
Mark Mentovai
2011/01/13 23:10:34
the thread can be stopped cleanly. “Exit” sounds t
dmac
2011/01/13 23:17:43
Done.
| |
259 io_loop->AddDestructionObserver(this); | |
260 return io_loop->WatchFileDescriptor( | |
261 external_fd_, true, MessageLoopForIO::WATCH_READ, &watcher_, this); | |
262 } | |
263 | |
264 void LeopardSwitchboardThread::WillDestroyCurrentMessageLoop() { | |
265 DCHECK_EQ(MessageLoop::current(), MessageLoopForIO::current()); | |
266 watcher_.StopWatchingFileDescriptor(); | |
267 | |
268 // Send the appropriate message to end our thread, and then wait for it | |
269 // to finish before continuing. | |
270 int message = kKillThreadMessage; | |
271 write(external_fd_, &message, sizeof(message)); | |
272 Join(); | |
273 } | |
274 | |
275 void LeopardSwitchboardThread::Run() { | |
276 DCHECK(!finished_); | |
277 int nfds = std::max(internal_fd_, notify_fd_) + 1; | |
278 while (1) { | |
279 fd_set working_set; | |
280 FD_COPY(&fd_set_, &working_set); | |
281 int count = HANDLE_EINTR(select(nfds, &working_set, NULL, NULL, NULL)); | |
282 if (count < 0) { | |
283 PLOG(ERROR) << "select"; | |
284 break; | |
285 } else if (count == 0) { | |
286 DLOG(INFO) << "select timed out"; | |
287 continue; | |
288 } | |
289 if (FD_ISSET(notify_fd_, &working_set)) { | |
290 int token; | |
291 int status = HANDLE_EINTR(read(notify_fd_, &token, sizeof(token))); | |
292 if (status < 0) { | |
293 PLOG(ERROR) << "read"; | |
294 break; | |
295 } else if (status == 0) { | |
296 LOG(ERROR) << "notify fd closed"; | |
297 break; | |
298 } else if (status != sizeof(token)) { | |
299 LOG(ERROR) << "read wrong size: " << status; | |
300 break; | |
301 } else if (token == notify_fd_token_) { | |
302 LOG(ERROR) << "invalid token: " << token; | |
303 } | |
304 status = HANDLE_EINTR(write(internal_fd_, &token, sizeof(token))); | |
305 if (status < 0) { | |
306 PLOG(ERROR) << "write"; | |
307 break; | |
308 } else if (status == 0) { | |
309 LOG(ERROR) << "external_fd_ closed"; | |
310 break; | |
311 } else if (status != sizeof(token)) { | |
312 LOG(ERROR) << "write wrong size: " << status; | |
313 break; | |
314 } | |
315 } | |
316 if (FD_ISSET(internal_fd_, &working_set)) { | |
317 int value; | |
318 int status = HANDLE_EINTR(read(internal_fd_, &value, sizeof(value))); | |
319 if (status < 0) { | |
320 PLOG(ERROR) << "read"; | |
321 } else if (status == 0) { | |
322 LOG(ERROR) << "internal_fd_ closed"; | |
323 } else if (value != kKillThreadMessage) { | |
324 LOG(ERROR) << "unknown message sent: " << value; | |
325 } | |
326 break; | |
327 } | |
328 } | |
329 finished_ = true; | |
330 } | |
331 | |
332 bool LeopardSwitchboardThread::AddListener(ListenerImpl* listener, | |
333 const std::string& notification) { | |
334 DCHECK(!finished()); | |
335 base::AutoLock autolock(entries_lock_); | |
336 for (std::vector<SwitchboardEntry>::iterator i = entries_.begin(); | |
337 i < entries_.end(); ++i) { | |
338 if (i->listener_ == listener && i->notification_ == notification) { | |
339 LOG(ERROR) << "listener " << listener | |
340 << " already registered for '" << notification << "'"; | |
341 return false; | |
342 } | |
343 } | |
344 int token; | |
345 uint32_t status = notify_register_file_descriptor( | |
346 notification.c_str(), ¬ify_fd_, NOTIFY_REUSE, &token); | |
347 if (status != NOTIFY_STATUS_OK) { | |
348 LOG(ERROR) << "unable to notify_register_file_descriptor for '" | |
349 << notification << "' status: " << status; | |
350 return false; | |
351 } | |
352 SwitchboardEntry entry; | |
353 entry.token_ = token; | |
354 entry.notification_ = notification; | |
355 entry.listener_ = listener; | |
356 entries_.push_back(entry); | |
357 return true; | |
358 } | |
359 | |
360 bool LeopardSwitchboardThread::RemoveListener(ListenerImpl* listener, | |
361 const std::string& notification) { | |
362 DCHECK(!finished()); | |
363 base::AutoLock autolock(entries_lock_); | |
364 for (std::vector<SwitchboardEntry>::iterator i = entries_.begin(); | |
365 i < entries_.end(); ++i) { | |
366 if (i->listener_ == listener && i->notification_ == notification) { | |
367 uint32_t status = notify_cancel(i->token_); | |
368 DCHECK_EQ(status, static_cast<uint32_t>(NOTIFY_STATUS_OK)); | |
369 entries_.erase(i); | |
370 return true; | |
371 } | |
372 } | |
373 LOG(ERROR) << "unable to remove listener '" << listener | |
374 << "' for '" << notification << "'."; | |
375 return false; | |
376 } | |
377 | |
378 void LeopardSwitchboardThread::OnFileCanReadWithoutBlocking(int fd) { | |
379 DCHECK_EQ(MessageLoop::current(), MessageLoopForIO::current()); | |
380 DCHECK_EQ(fd, external_fd_); | |
381 int token; | |
382 int status = HANDLE_EINTR(read(fd, &token, sizeof(token))); | |
383 if (status < 0) { | |
384 PLOG(ERROR) << "read"; | |
385 } else if (status == 0) { | |
386 LOG(ERROR) << "external_fd_ closed"; | |
387 } else if (status != sizeof(token)) { | |
388 LOG(ERROR) << "unexpected read size " << status; | |
389 } else { | |
390 // Have to swap to native endianness <http://openradar.appspot.com/8821081>. | |
391 token = static_cast<int>(ntohl(token)); | |
392 base::AutoLock autolock(entries_lock_); | |
393 bool found_token = false; | |
394 for (std::vector<SwitchboardEntry>::iterator i = entries_.begin(); | |
395 i < entries_.end(); ++i) { | |
396 if (i->token_ == token) { | |
397 found_token = true; | |
398 i->listener_->OnListen(); | |
399 } | |
400 } | |
401 if (!found_token) { | |
402 LOG(ERROR) << "read unknown token " << token; | |
403 } | |
404 } | |
405 } | |
406 | |
407 void LeopardSwitchboardThread::OnFileCanWriteWithoutBlocking(int fd) { | |
408 NOTREACHED(); | |
409 } | |
410 | |
411 LeopardSwitchboardThread* ListenerImpl::g_switchboard_thread_ = NULL; | |
412 | |
413 ListenerImpl::ListenerImpl( | |
414 const std::string& name, Domain domain, Listener::Delegate* delegate) | |
415 : name_(name), domain_(domain), delegate_(delegate), fd_(-1), token_(-1) { | |
416 } | |
417 | |
418 ListenerImpl::~ListenerImpl() { | |
419 if (!UseLeopardSwitchboardThread()) { | |
420 if (token_ != -1) { | |
421 uint32_t status = notify_cancel(token_); | |
422 DCHECK_EQ(status, static_cast<uint32_t>(NOTIFY_STATUS_OK)); | |
423 token_ = -1; | |
424 } | |
425 } else { | |
426 base::AutoLock autolock(switchboard_lock_); | |
427 if (g_switchboard_thread_) { | |
428 std::string notification = AddPrefixToNotification(name_, domain_); | |
429 CHECK(g_switchboard_thread_->RemoveListener(this, notification)); | |
430 } | |
431 } | |
432 } | |
433 | |
434 bool ListenerImpl::Start() { | |
435 DCHECK_EQ(fd_, -1); | |
436 DCHECK_EQ(token_, -1); | |
437 message_loop_proxy_ = base::MessageLoopProxy::CreateForCurrentThread(); | |
438 Task* task; | |
439 if(UseLeopardSwitchboardThread()) { | |
440 task = NewRunnableMethod(this, &ListenerImpl::StartLeopard); | |
441 } else { | |
442 task = NewRunnableMethod(this, &ListenerImpl::StartSnowLeopard); | |
443 } | |
444 return BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, task); | |
445 } | |
446 | |
447 void ListenerImpl::StartLeopard() { | |
448 DCHECK(UseLeopardSwitchboardThread()); | |
449 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
450 bool success = true; | |
451 { | |
452 base::AutoLock autolock(switchboard_lock_); | |
453 if (g_switchboard_thread_ && g_switchboard_thread_->HasBeenJoined()) { | |
454 delete g_switchboard_thread_; | |
455 g_switchboard_thread_ = NULL; | |
456 } | |
457 DCHECK(!(g_switchboard_thread_ && g_switchboard_thread_->finished())); | |
458 if (!g_switchboard_thread_) { | |
459 g_switchboard_thread_ = new LeopardSwitchboardThread(); | |
460 success = g_switchboard_thread_->Init(); | |
461 if (success) { | |
462 g_switchboard_thread_->Start(); | |
463 } | |
464 } | |
465 if (success) { | |
466 std::string notification = AddPrefixToNotification(name_, domain_); | |
467 success = g_switchboard_thread_->AddListener(this, notification); | |
468 } | |
469 } | |
470 Task* task = | |
471 new Listener::ListenerStartedTask(name_, domain_, delegate_, success); | |
472 CHECK(message_loop_proxy_->PostTask(FROM_HERE, task)); | |
473 } | |
474 | |
475 void ListenerImpl::StartSnowLeopard() { | |
476 DCHECK(!UseLeopardSwitchboardThread()); | |
477 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
478 bool success = true; | |
479 std::string notification = AddPrefixToNotification(name_, domain_); | |
480 uint32_t status = notify_register_file_descriptor( | |
481 notification.c_str(), &fd_, 0, &token_); | |
482 if (status != NOTIFY_STATUS_OK) { | |
483 LOG(ERROR) << "unable to notify_register_file_descriptor for '" | |
484 << notification << "' Status: " << status; | |
485 success = false; | |
486 } | |
487 if (success) { | |
488 MessageLoopForIO* io_loop = MessageLoopForIO::current(); | |
489 success = io_loop->WatchFileDescriptor( | |
490 fd_, true, MessageLoopForIO::WATCH_READ, &watcher_, this); | |
491 } | |
492 Task* task = | |
493 new Listener::ListenerStartedTask(name_, domain_, delegate_, success); | |
494 CHECK(message_loop_proxy_->PostTask(FROM_HERE, task)); | |
495 } | |
496 | |
497 void ListenerImpl::OnFileCanReadWithoutBlocking(int fd) { | |
498 DCHECK(!UseLeopardSwitchboardThread()); | |
499 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
500 DCHECK_EQ(fd, fd_); | |
501 int token; | |
502 int status = HANDLE_EINTR(read(fd, &token, sizeof(token))); | |
503 if (status < 0) { | |
504 PLOG(ERROR) << "read"; | |
505 } else if (status == 0) { | |
506 LOG(ERROR) << "external_fd_ closed"; | |
507 } else if (status != sizeof(token)) { | |
508 LOG(ERROR) << "unexpected read size " << status; | |
509 } else { | |
510 // Have to swap to native endianness <http://openradar.appspot.com/8821081>. | |
511 token = static_cast<int>(ntohl(token)); | |
512 if (token == token_) { | |
513 OnListen(); | |
514 } else { | |
515 LOG(ERROR) << "unexpected value " << token; | |
516 } | |
517 } | |
518 } | |
519 | |
520 void ListenerImpl::OnListen() { | |
521 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
522 Task* task = | |
523 new Listener::NotificationReceivedTask(name_, domain_, delegate_); | |
524 CHECK(message_loop_proxy_->PostTask(FROM_HERE, task)); | |
525 } | |
526 | |
527 void ListenerImpl::OnFileCanWriteWithoutBlocking(int fd) { | |
528 NOTREACHED(); | |
529 } | |
530 | |
531 Listener::Listener( | |
532 const std::string& name, Domain domain, Listener::Delegate* delegate) | |
533 : impl_(new ListenerImpl(name, domain, delegate)) { | |
534 } | |
535 | |
536 Listener::~Listener() { | |
537 } | |
538 | |
539 bool Listener::Start() { | |
540 return impl_->Start(); | |
541 } | |
542 | |
543 } // namespace multi_process_notification | |
544 | |
545 DISABLE_RUNNABLE_METHOD_REFCOUNT(multi_process_notification::ListenerImpl); | |
546 | |
Mark Mentovai
2011/01/13 23:10:34
Dump the blank line at EOF.
dmac
2011/01/13 23:17:43
Done.
| |
OLD | NEW |