Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(202)

Side by Side Diff: remoting/host/remoting_me2me_host.cc

Issue 10829467: [Chromoting] Introducing refcount-based life time management of the message loops in the service (d… (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Destructors of ref-counted objects should not be public. Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 // 4 //
5 // This file implements a standalone host process for Me2Me. 5 // This file implements a standalone host process for Me2Me.
6 6
7 #include <string> 7 #include <string>
8 8
9 #include "base/at_exit.h" 9 #include "base/at_exit.h"
10 #include "base/bind.h" 10 #include "base/bind.h"
(...skipping 10 matching lines...) Expand all
21 #include "base/synchronization/waitable_event.h" 21 #include "base/synchronization/waitable_event.h"
22 #include "base/threading/thread.h" 22 #include "base/threading/thread.h"
23 #include "base/utf_string_conversions.h" 23 #include "base/utf_string_conversions.h"
24 #include "base/win/windows_version.h" 24 #include "base/win/windows_version.h"
25 #include "build/build_config.h" 25 #include "build/build_config.h"
26 #include "crypto/nss_util.h" 26 #include "crypto/nss_util.h"
27 #include "ipc/ipc_channel.h" 27 #include "ipc/ipc_channel.h"
28 #include "ipc/ipc_channel_proxy.h" 28 #include "ipc/ipc_channel_proxy.h"
29 #include "net/base/network_change_notifier.h" 29 #include "net/base/network_change_notifier.h"
30 #include "net/socket/ssl_server_socket.h" 30 #include "net/socket/ssl_server_socket.h"
31 #include "remoting/base/auto_message_loop.h"
31 #include "remoting/base/breakpad.h" 32 #include "remoting/base/breakpad.h"
32 #include "remoting/base/constants.h" 33 #include "remoting/base/constants.h"
33 #include "remoting/host/branding.h" 34 #include "remoting/host/branding.h"
34 #include "remoting/host/chromoting_host.h" 35 #include "remoting/host/chromoting_host.h"
35 #include "remoting/host/chromoting_host_context.h" 36 #include "remoting/host/chromoting_host_context.h"
36 #include "remoting/host/composite_host_config.h" 37 #include "remoting/host/composite_host_config.h"
37 #include "remoting/host/constants.h" 38 #include "remoting/host/constants.h"
38 #include "remoting/host/desktop_environment.h" 39 #include "remoting/host/desktop_environment.h"
39 #include "remoting/host/event_executor.h" 40 #include "remoting/host/event_executor.h"
40 #include "remoting/host/heartbeat_sender.h" 41 #include "remoting/host/heartbeat_sender.h"
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
98 const char kOfficialOAuth2ClientSecret[] = "Bgur6DFiOMM1h8x-AQpuTQlK"; 99 const char kOfficialOAuth2ClientSecret[] = "Bgur6DFiOMM1h8x-AQpuTQlK";
99 100
100 } // namespace 101 } // namespace
101 102
102 namespace remoting { 103 namespace remoting {
103 104
104 class HostProcess 105 class HostProcess
105 : public HeartbeatSender::Listener, 106 : public HeartbeatSender::Listener,
106 public IPC::Listener { 107 public IPC::Listener {
107 public: 108 public:
108 HostProcess() 109 HostProcess(scoped_ptr<ChromotingHostContext> context)
109 : message_loop_(MessageLoop::TYPE_UI), 110 : context_(context.Pass()),
110 #ifdef OFFICIAL_BUILD 111 #ifdef OFFICIAL_BUILD
111 oauth_use_official_client_id_(true), 112 oauth_use_official_client_id_(true),
112 #else 113 #else
113 oauth_use_official_client_id_(false), 114 oauth_use_official_client_id_(false),
114 #endif 115 #endif
115 allow_nat_traversal_(true), 116 allow_nat_traversal_(true),
116 restarting_(false), 117 restarting_(false),
117 shutting_down_(false), 118 shutting_down_(false),
118 exit_code_(kSuccessExitCode) 119 exit_code_(kSuccessExitCode)
119 #if defined(OS_MACOSX) 120 #if defined(OS_MACOSX)
120 , curtain_(base::Bind(&HostProcess::OnDisconnectRequested, 121 , curtain_(base::Bind(&HostProcess::OnDisconnectRequested,
121 base::Unretained(this)), 122 base::Unretained(this)),
122 base::Bind(&HostProcess::OnDisconnectRequested, 123 base::Bind(&HostProcess::OnDisconnectRequested,
123 base::Unretained(this))) 124 base::Unretained(this)))
124 #endif 125 #endif
125 { 126 {
126 context_.reset(
127 new ChromotingHostContext(message_loop_.message_loop_proxy()));
128 context_->Start();
129 network_change_notifier_.reset(net::NetworkChangeNotifier::Create()); 127 network_change_notifier_.reset(net::NetworkChangeNotifier::Create());
130 config_updated_timer_.reset(new base::DelayTimer<HostProcess>( 128 config_updated_timer_.reset(new base::DelayTimer<HostProcess>(
131 FROM_HERE, base::TimeDelta::FromSeconds(2), this, 129 FROM_HERE, base::TimeDelta::FromSeconds(2), this,
132 &HostProcess::ConfigUpdatedDelayed)); 130 &HostProcess::ConfigUpdatedDelayed));
133 } 131 }
134 132
135 bool InitWithCommandLine(const CommandLine* cmd_line) { 133 bool InitWithCommandLine(const CommandLine* cmd_line) {
136 // Connect to the daemon process. 134 // Connect to the daemon process.
137 std::string channel_name = 135 std::string channel_name =
138 cmd_line->GetSwitchValueASCII(kDaemonIpcSwitchName); 136 cmd_line->GetSwitchValueASCII(kDaemonIpcSwitchName);
(...skipping 18 matching lines...) Expand all
157 host_config_path_ = default_config_dir.Append(kDefaultHostConfigFile); 155 host_config_path_ = default_config_dir.Append(kDefaultHostConfigFile);
158 if (cmd_line->HasSwitch(kHostConfigSwitchName)) { 156 if (cmd_line->HasSwitch(kHostConfigSwitchName)) {
159 host_config_path_ = cmd_line->GetSwitchValuePath(kHostConfigSwitchName); 157 host_config_path_ = cmd_line->GetSwitchValuePath(kHostConfigSwitchName);
160 } 158 }
161 config_.AddConfigPath(host_config_path_); 159 config_.AddConfigPath(host_config_path_);
162 160
163 return true; 161 return true;
164 } 162 }
165 163
166 void ConfigUpdated() { 164 void ConfigUpdated() {
167 DCHECK(message_loop_.message_loop_proxy()->BelongsToCurrentThread()); 165 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
168 166
169 // Call ConfigUpdatedDelayed after a short delay, so that this object won't 167 // Call ConfigUpdatedDelayed after a short delay, so that this object won't
170 // try to read the updated configuration file before it has been 168 // try to read the updated configuration file before it has been
171 // completely written. 169 // completely written.
172 // If the writer moves the new configuration file into place atomically, 170 // If the writer moves the new configuration file into place atomically,
173 // this delay may not be necessary. 171 // this delay may not be necessary.
174 config_updated_timer_->Reset(); 172 config_updated_timer_->Reset();
175 } 173 }
176 174
177 void ConfigUpdatedDelayed() { 175 void ConfigUpdatedDelayed() {
178 DCHECK(message_loop_.message_loop_proxy()->BelongsToCurrentThread()); 176 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
179 177
180 if (LoadConfig()) { 178 if (LoadConfig()) {
181 // PostTask to create new authenticator factory in case PIN has changed. 179 // PostTask to create new authenticator factory in case PIN has changed.
182 context_->network_task_runner()->PostTask( 180 context_->network_task_runner()->PostTask(
183 FROM_HERE, 181 FROM_HERE,
184 base::Bind(&HostProcess::CreateAuthenticatorFactory, 182 base::Bind(&HostProcess::CreateAuthenticatorFactory,
185 base::Unretained(this))); 183 base::Unretained(this)));
186 } else { 184 } else {
187 LOG(ERROR) << "Invalid configuration."; 185 LOG(ERROR) << "Invalid configuration.";
188 } 186 }
(...skipping 24 matching lines...) Expand all
213 }; 211 };
214 #endif // defined(OS_WIN) 212 #endif // defined(OS_WIN)
215 213
216 void ListenForConfigChanges() { 214 void ListenForConfigChanges() {
217 #if defined(OS_POSIX) 215 #if defined(OS_POSIX)
218 remoting::RegisterHupSignalHandler( 216 remoting::RegisterHupSignalHandler(
219 base::Bind(&HostProcess::ConfigUpdatedDelayed, base::Unretained(this))); 217 base::Bind(&HostProcess::ConfigUpdatedDelayed, base::Unretained(this)));
220 #elif defined(OS_WIN) 218 #elif defined(OS_WIN)
221 scoped_refptr<base::files::FilePathWatcher::Delegate> delegate( 219 scoped_refptr<base::files::FilePathWatcher::Delegate> delegate(
222 new ConfigChangedDelegate( 220 new ConfigChangedDelegate(
223 message_loop_.message_loop_proxy(), 221 context_->ui_task_runner(),
224 base::Bind(&HostProcess::ConfigUpdated, base::Unretained(this)))); 222 base::Bind(&HostProcess::ConfigUpdated, base::Unretained(this))));
225 config_watcher_.reset(new base::files::FilePathWatcher()); 223 config_watcher_.reset(new base::files::FilePathWatcher());
226 if (!config_watcher_->Watch(host_config_path_, delegate)) { 224 if (!config_watcher_->Watch(host_config_path_, delegate)) {
227 LOG(ERROR) << "Couldn't watch file " << host_config_path_.value(); 225 LOG(ERROR) << "Couldn't watch file " << host_config_path_.value();
228 } 226 }
229 #endif // defined (OS_WIN) 227 #endif // defined (OS_WIN)
230 } 228 }
231 229
232 void CreateAuthenticatorFactory() { 230 void CreateAuthenticatorFactory() {
233 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); 231 DCHECK(context_->network_task_runner()->BelongsToCurrentThread());
234 scoped_ptr<protocol::AuthenticatorFactory> factory( 232 scoped_ptr<protocol::AuthenticatorFactory> factory(
235 new protocol::Me2MeHostAuthenticatorFactory( 233 new protocol::Me2MeHostAuthenticatorFactory(
236 key_pair_.GenerateCertificate(), 234 key_pair_.GenerateCertificate(),
237 *key_pair_.private_key(), host_secret_hash_)); 235 *key_pair_.private_key(), host_secret_hash_));
238 host_->SetAuthenticatorFactory(factory.Pass()); 236 host_->SetAuthenticatorFactory(factory.Pass());
239 } 237 }
240 238
241 // IPC::Listener implementation. 239 // IPC::Listener implementation.
242 virtual bool OnMessageReceived(const IPC::Message& message) { 240 virtual bool OnMessageReceived(const IPC::Message& message) {
243 return false; 241 return false;
244 } 242 }
245 243
246 int Run() { 244 void StartHostProcess() {
247 if (!LoadConfig()) { 245 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
248 return kInvalidHostConfigurationExitCode; 246
247 if (!InitWithCommandLine(CommandLine::ForCurrentProcess()) ||
248 !LoadConfig()) {
249 context_->network_task_runner()->PostTask(
250 FROM_HERE,
251 base::Bind(&HostProcess::Shutdown, base::Unretained(this),
252 kInvalidHostConfigurationExitCode));
253 return;
249 } 254 }
250 255
251 #if defined(OS_MACOSX) || defined(OS_WIN) 256 #if defined(OS_MACOSX) || defined(OS_WIN)
252 host_user_interface_.reset(new HostUserInterface(context_.get())); 257 host_user_interface_.reset(new HostUserInterface(context_.get()));
253 #endif 258 #endif
254 259
255 StartWatchingPolicy(); 260 StartWatchingPolicy();
256 261
257 #if defined(OS_MACOSX) || defined(OS_WIN) 262 #if defined(OS_MACOSX) || defined(OS_WIN)
258 context_->file_task_runner()->PostTask( 263 context_->file_task_runner()->PostTask(
259 FROM_HERE, 264 FROM_HERE,
260 base::Bind(&HostProcess::ListenForConfigChanges, 265 base::Bind(&HostProcess::ListenForConfigChanges,
261 base::Unretained(this))); 266 base::Unretained(this)));
262 #endif 267 #endif
263 message_loop_.Run(); 268 }
269
270 void ShutdownHostProcess() {
271 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
272
273 daemon_channel_.reset();
264 274
265 #if defined(OS_MACOSX) || defined(OS_WIN) 275 #if defined(OS_MACOSX) || defined(OS_WIN)
266 host_user_interface_.reset(); 276 host_user_interface_.reset();
267 #endif 277 #endif
268 278
269 daemon_channel_.reset(); 279 if (policy_watcher_.get()) {
270 base::WaitableEvent done_event(true, false); 280 base::WaitableEvent done_event(true, false);
271 policy_watcher_->StopWatching(&done_event); 281 policy_watcher_->StopWatching(&done_event);
272 done_event.Wait(); 282 done_event.Wait();
273 policy_watcher_.reset(); 283 policy_watcher_.reset();
284 }
274 285
286 context_.reset();
287 }
288
289 int Run(MessageLoop* message_loop) {
290 message_loop->Run();
275 return exit_code_; 291 return exit_code_;
276 } 292 }
277 293
278 // Overridden from HeartbeatSender::Listener 294 // Overridden from HeartbeatSender::Listener
279 virtual void OnUnknownHostIdError() OVERRIDE { 295 virtual void OnUnknownHostIdError() OVERRIDE {
280 LOG(ERROR) << "Host ID not found."; 296 LOG(ERROR) << "Host ID not found.";
281 Shutdown(kInvalidHostIdExitCode); 297 Shutdown(kInvalidHostIdExitCode);
282 } 298 }
283 299
284 private: 300 private:
285 void StartWatchingPolicy() { 301 void StartWatchingPolicy() {
286 policy_watcher_.reset( 302 policy_watcher_.reset(
287 policy_hack::PolicyWatcher::Create(context_->file_task_runner())); 303 policy_hack::PolicyWatcher::Create(context_->file_task_runner()));
288 policy_watcher_->StartWatching( 304 policy_watcher_->StartWatching(
289 base::Bind(&HostProcess::OnPolicyUpdate, base::Unretained(this))); 305 base::Bind(&HostProcess::OnPolicyUpdate, base::Unretained(this)));
290 } 306 }
291 307
292 // Read host config, returning true if successful. 308 // Read host config, returning true if successful.
293 bool LoadConfig() { 309 bool LoadConfig() {
294 DCHECK(message_loop_.message_loop_proxy()->BelongsToCurrentThread()); 310 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
295 311
296 // TODO(sergeyu): There is a potential race condition: this function is 312 // TODO(sergeyu): There is a potential race condition: this function is
297 // called on the main thread while the class members it mutates are used on 313 // called on the main thread while the class members it mutates are used on
298 // the network thread. Fix it. http://crbug.com/140986 . 314 // the network thread. Fix it. http://crbug.com/140986 .
299 315
300 if (!config_.Read()) { 316 if (!config_.Read()) {
301 LOG(ERROR) << "Failed to read config file."; 317 LOG(ERROR) << "Failed to read config file.";
302 return false; 318 return false;
303 } 319 }
304 320
(...skipping 225 matching lines...) Expand 10 before | Expand all | Expand 10 after
530 CreateAuthenticatorFactory(); 546 CreateAuthenticatorFactory();
531 } 547 }
532 548
533 void OnAuthFailed() { 549 void OnAuthFailed() {
534 Shutdown(kInvalidOauthCredentialsExitCode); 550 Shutdown(kInvalidOauthCredentialsExitCode);
535 } 551 }
536 552
537 // Invoked when the user uses the Disconnect windows to terminate 553 // Invoked when the user uses the Disconnect windows to terminate
538 // the sessions. 554 // the sessions.
539 void OnDisconnectRequested() { 555 void OnDisconnectRequested() {
540 DCHECK(message_loop_.message_loop_proxy()->BelongsToCurrentThread()); 556 DCHECK(context_->ui_task_runner()->BelongsToCurrentThread());
541 557
542 host_->DisconnectAllClients(); 558 host_->DisconnectAllClients();
543 } 559 }
544 560
545 void RestartHost() { 561 void RestartHost() {
546 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); 562 DCHECK(context_->network_task_runner()->BelongsToCurrentThread());
547 563
548 if (restarting_ || shutting_down_) 564 if (restarting_ || shutting_down_)
549 return; 565 return;
550 566
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
588 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); 604 DCHECK(context_->network_task_runner()->BelongsToCurrentThread());
589 605
590 // Destroy networking objects while we are on the network thread. 606 // Destroy networking objects while we are on the network thread.
591 host_ = NULL; 607 host_ = NULL;
592 host_event_logger_.reset(); 608 host_event_logger_.reset();
593 log_to_server_.reset(); 609 log_to_server_.reset();
594 heartbeat_sender_.reset(); 610 heartbeat_sender_.reset();
595 signaling_connector_.reset(); 611 signaling_connector_.reset();
596 signal_strategy_.reset(); 612 signal_strategy_.reset();
597 613
598 message_loop_.PostTask(FROM_HERE, MessageLoop::QuitClosure()); 614 // Complete the rest of shutdown on the main thread.
615 context_->ui_task_runner()->PostTask(
616 FROM_HERE,
617 base::Bind(&HostProcess::ShutdownHostProcess,
618 base::Unretained(this)));
599 } 619 }
600 620
601 MessageLoop message_loop_;
602 scoped_ptr<ChromotingHostContext> context_; 621 scoped_ptr<ChromotingHostContext> context_;
603 scoped_ptr<IPC::ChannelProxy> daemon_channel_; 622 scoped_ptr<IPC::ChannelProxy> daemon_channel_;
604 scoped_ptr<net::NetworkChangeNotifier> network_change_notifier_; 623 scoped_ptr<net::NetworkChangeNotifier> network_change_notifier_;
605 624
606 FilePath host_config_path_; 625 FilePath host_config_path_;
607 CompositeHostConfig config_; 626 CompositeHostConfig config_;
608 627
609 std::string host_id_; 628 std::string host_id_;
610 HostKeyPair key_pair_; 629 HostKeyPair key_pair_;
611 protocol::SharedSecretHash host_secret_hash_; 630 protocol::SharedSecretHash host_secret_hash_;
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
665 InitLogging(debug_log.value().c_str(), 684 InitLogging(debug_log.value().c_str(),
666 #if defined(OS_WIN) 685 #if defined(OS_WIN)
667 logging::LOG_ONLY_TO_FILE, 686 logging::LOG_ONLY_TO_FILE,
668 #else 687 #else
669 logging::LOG_ONLY_TO_SYSTEM_DEBUG_LOG, 688 logging::LOG_ONLY_TO_SYSTEM_DEBUG_LOG,
670 #endif 689 #endif
671 logging::DONT_LOCK_LOG_FILE, 690 logging::DONT_LOCK_LOG_FILE,
672 logging::APPEND_TO_OLD_LOG_FILE, 691 logging::APPEND_TO_OLD_LOG_FILE,
673 logging::DISABLE_DCHECK_FOR_NON_OFFICIAL_RELEASE_BUILDS); 692 logging::DISABLE_DCHECK_FOR_NON_OFFICIAL_RELEASE_BUILDS);
674 693
675 const CommandLine* cmd_line = CommandLine::ForCurrentProcess();
676
677 #if defined(TOOLKIT_GTK) 694 #if defined(TOOLKIT_GTK)
678 // Required for any calls into GTK functions, such as the Disconnect and 695 // Required for any calls into GTK functions, such as the Disconnect and
679 // Continue windows, though these should not be used for the Me2Me case 696 // Continue windows, though these should not be used for the Me2Me case
680 // (crbug.com/104377). 697 // (crbug.com/104377).
698 const CommandLine* cmd_line = CommandLine::ForCurrentProcess();
681 gfx::GtkInitFromCommandLine(*cmd_line); 699 gfx::GtkInitFromCommandLine(*cmd_line);
682 #endif // TOOLKIT_GTK 700 #endif // TOOLKIT_GTK
683 701
684 // Enable support for SSL server sockets, which must be done while still 702 // Enable support for SSL server sockets, which must be done while still
685 // single-threaded. 703 // single-threaded.
686 net::EnableSSLServerSockets(); 704 net::EnableSSLServerSockets();
687 705
688 #if defined(OS_LINUX) 706 #if defined(OS_LINUX)
689 remoting::VideoFrameCapturer::EnableXDamage(true); 707 remoting::VideoFrameCapturer::EnableXDamage(true);
690 #endif 708 #endif
691 709
692 remoting::HostProcess me2me_host; 710 // Create the main message loop and start helper threads.
693 if (!me2me_host.InitWithCommandLine(cmd_line)) { 711 MessageLoop message_loop(MessageLoop::TYPE_UI);
694 return remoting::kInvalidHostConfigurationExitCode; 712 scoped_ptr<remoting::ChromotingHostContext> context(
695 } 713 new remoting::ChromotingHostContext(
714 new remoting::AutoMessageLoop(&message_loop)));
715 if (!context->Start())
716 return remoting::kHostInitializationFailed;
696 717
697 return me2me_host.Run(); 718 // Create the host process instance and run the rest of the initialization on
719 // the main message loop.
720 remoting::HostProcess me2me_host(context.Pass());
721 message_loop.PostTask(
722 FROM_HERE,
723 base::Bind(&remoting::HostProcess::StartHostProcess,
724 base::Unretained(&me2me_host)));
725 return me2me_host.Run(&message_loop);
Wez 2012/08/24 21:30:49 Why pass the raw MessageLoop in to Run(), rather t
alexeypa (please no reviews) 2012/08/27 21:19:40 SingleThreadTaskRunner does not expose Run() metho
Wez 2012/08/28 17:34:53 Right, but that's an artefact of the way HostProce
alexeypa (please no reviews) 2012/08/28 19:18:51 Yes, indeed. Done.
698 } 726 }
699 727
700 #if defined(OS_WIN) 728 #if defined(OS_WIN)
701 HMODULE g_hModule = NULL; 729 HMODULE g_hModule = NULL;
702 730
703 int CALLBACK WinMain(HINSTANCE instance, 731 int CALLBACK WinMain(HINSTANCE instance,
704 HINSTANCE previous_instance, 732 HINSTANCE previous_instance,
705 LPSTR command_line, 733 LPSTR command_line,
706 int show_command) { 734 int show_command) {
707 #ifdef OFFICIAL_BUILD 735 #ifdef OFFICIAL_BUILD
(...skipping 23 matching lines...) Expand all
731 user32.GetFunctionPointer("SetProcessDPIAware")); 759 user32.GetFunctionPointer("SetProcessDPIAware"));
732 set_process_dpi_aware(); 760 set_process_dpi_aware();
733 } 761 }
734 762
735 // CommandLine::Init() ignores the passed |argc| and |argv| on Windows getting 763 // CommandLine::Init() ignores the passed |argc| and |argv| on Windows getting
736 // the command line from GetCommandLineW(), so we can safely pass NULL here. 764 // the command line from GetCommandLineW(), so we can safely pass NULL here.
737 return main(0, NULL); 765 return main(0, NULL);
738 } 766 }
739 767
740 #endif // defined(OS_WIN) 768 #endif // defined(OS_WIN)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698