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

Side by Side Diff: ppapi/native_client/src/trusted/plugin/service_runtime.cc

Issue 249183004: Implement open_resource in non-SFI mode. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 6 years, 8 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 /* 1 /*
2 * Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 * Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be 3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file. 4 * found in the LICENSE file.
5 */ 5 */
6 6
7 #define NACL_LOG_MODULE_NAME "Plugin_ServiceRuntime" 7 #define NACL_LOG_MODULE_NAME "Plugin_ServiceRuntime"
8 8
9 #include "ppapi/native_client/src/trusted/plugin/service_runtime.h" 9 #include "ppapi/native_client/src/trusted/plugin/service_runtime.h"
10 10
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
57 57
58 namespace plugin { 58 namespace plugin {
59 namespace { 59 namespace {
60 60
61 // For doing crude quota enforcement on writes to temp files. 61 // For doing crude quota enforcement on writes to temp files.
62 // We do not allow a temp file bigger than 128 MB for now. 62 // We do not allow a temp file bigger than 128 MB for now.
63 // There is currently a limit of 32M for nexe text size, so 128M 63 // There is currently a limit of 32M for nexe text size, so 128M
64 // should be plenty for static data 64 // should be plenty for static data
65 const int64_t kMaxTempQuota = 0x8000000; 65 const int64_t kMaxTempQuota = 0x8000000;
66 66
67 class OpenManifestEntryAsyncCallback {
68 public:
69 OpenManifestEntryAsyncCallback(PP_FileHandle* file_handle,
70 const pp::CompletionCallback& callback)
71 : file_handle_(file_handle),
72 callback_(callback) {
73 }
74
75 ~OpenManifestEntryAsyncCallback() {
76 if (callback_.pp_completion_callback().func)
77 callback_.RunAndClear(PP_ERROR_ABORTED);
78 }
79
80 void Run(int32_t pp_error) {
81 #if defined(OS_WIN)
82 // Currently, this is used only for non-SFI mode, and now the mode is not
83 // supproted on windows.
dmichael (off chromium) 2014/04/25 21:06:58 supproted->supported
hidehiko 2014/04/28 08:44:27 Done.
84 // TODO(hidehiko): Support it on Windows when we switch to use
85 // ManifestService also in SFI-mode.
86 NACL_NOTREACHED();
87 #elif defined(OS_POSIX)
88 // On posix, PlatformFile is the file descriptor.
89 *file_handle_ = (pp_error == PP_OK) ? info_.desc : -1;
90 #endif
91 callback_.RunAndClear(pp_error);
92 delete this;
93 }
94
95 NaClFileInfo* mutable_info() { return &info_; }
96 pp::CompletionCallback pp_completion_callback() {
dmichael (off chromium) 2014/04/25 21:06:58 I find this confusing, that this class deals with
hidehiko 2014/04/28 08:44:27 Done, but to be honest it looks slightly layer-vio
97 return pp::CompletionCallback(&RunTrampoline, this);
98 }
99
100 private:
101 static void RunTrampoline(void* user_data, int32_t pp_error) {
102 static_cast<OpenManifestEntryAsyncCallback*>(user_data)->Run(pp_error);
103 }
104
105 PP_FileHandle* file_handle_;
106 NaClFileInfo info_;
107 pp::CompletionCallback callback_;
108 DISALLOW_COPY_AND_ASSIGN(OpenManifestEntryAsyncCallback);
109 };
110
67 class ManifestService { 111 class ManifestService {
68 public: 112 public:
69 ManifestService(nacl::WeakRefAnchor* anchor, 113 ManifestService(nacl::WeakRefAnchor* anchor,
70 PluginReverseInterface* plugin_reverse) 114 PluginReverseInterface* plugin_reverse)
71 : anchor_(anchor), 115 : anchor_(anchor),
72 plugin_reverse_(plugin_reverse) { 116 plugin_reverse_(plugin_reverse) {
73 } 117 }
74 118
75 ~ManifestService() { 119 ~ManifestService() {
76 anchor_->Unref(); 120 anchor_->Unref();
77 } 121 }
78 122
79 bool Quit() { 123 bool Quit() {
80 delete this; 124 delete this;
81 return false; 125 return false;
82 } 126 }
83 127
84 bool StartupInitializationComplete() { 128 bool StartupInitializationComplete() {
85 // Release this instance if the ServiceRuntime is already destructed. 129 // Release this instance if the ServiceRuntime is already destructed.
86 if (anchor_->is_abandoned()) { 130 if (anchor_->is_abandoned()) {
87 delete this; 131 delete this;
88 return false; 132 return false;
89 } 133 }
90 134
91 plugin_reverse_->StartupInitializationComplete(); 135 plugin_reverse_->StartupInitializationComplete();
92 return true; 136 return true;
93 } 137 }
94 138
139 bool OpenResource(const char* entry_key, PP_FileHandle* file,
140 const pp::CompletionCallback& callback) {
141 // Release this instance if the ServiceRuntime is already destructed.
142 if (anchor_->is_abandoned()) {
143 delete this;
144 return false;
145 }
146
147 OpenManifestEntryAsyncCallback* open_manifest_callback =
148 new OpenManifestEntryAsyncCallback(file, callback);
149 plugin_reverse_->OpenManifestEntryAsync(
150 entry_key,
151 open_manifest_callback->mutable_info(),
152 open_manifest_callback->pp_completion_callback());
153 return true;
154 }
155
95 static PP_Bool QuitTrampoline(void* user_data) { 156 static PP_Bool QuitTrampoline(void* user_data) {
96 return PP_FromBool(static_cast<ManifestService*>(user_data)->Quit()); 157 return PP_FromBool(static_cast<ManifestService*>(user_data)->Quit());
97 } 158 }
98 159
99 static PP_Bool StartupInitializationCompleteTrampoline(void* user_data) { 160 static PP_Bool StartupInitializationCompleteTrampoline(void* user_data) {
100 return PP_FromBool(static_cast<ManifestService*>(user_data)-> 161 return PP_FromBool(static_cast<ManifestService*>(user_data)->
101 StartupInitializationComplete()); 162 StartupInitializationComplete());
102 } 163 }
103 164
165 static PP_Bool OpenResourceTrampoline(
166 void* user_data, const char* entry_key, PP_FileHandle* file,
167 struct PP_CompletionCallback callback) {
168 return PP_FromBool(
169 static_cast<ManifestService*>(user_data)->OpenResource(
170 entry_key, file,
171 pp::CompletionCallback(callback.func, callback.user_data)));
172 }
173
104 private: 174 private:
105 // Weak reference to check if plugin_reverse is legally accessible or not. 175 // Weak reference to check if plugin_reverse is legally accessible or not.
106 nacl::WeakRefAnchor* anchor_; 176 nacl::WeakRefAnchor* anchor_;
107 PluginReverseInterface* plugin_reverse_; 177 PluginReverseInterface* plugin_reverse_;
108 178
109 DISALLOW_COPY_AND_ASSIGN(ManifestService); 179 DISALLOW_COPY_AND_ASSIGN(ManifestService);
110 }; 180 };
111 181
112 // Vtable to pass functions to LaunchSelLdr. 182 // Vtable to pass functions to LaunchSelLdr.
113 const PP_ManifestService kManifestServiceVTable = { 183 const PPP_ManifestService kManifestServiceVTable = {
114 &ManifestService::QuitTrampoline, 184 &ManifestService::QuitTrampoline,
115 &ManifestService::StartupInitializationCompleteTrampoline, 185 &ManifestService::StartupInitializationCompleteTrampoline,
186 &ManifestService::OpenResourceTrampoline,
116 }; 187 };
117 188
118 } // namespace 189 } // namespace
119 190
120 PluginReverseInterface::PluginReverseInterface( 191 PluginReverseInterface::PluginReverseInterface(
121 nacl::WeakRefAnchor* anchor, 192 nacl::WeakRefAnchor* anchor,
122 Plugin* plugin, 193 Plugin* plugin,
123 const Manifest* manifest, 194 const Manifest* manifest,
124 ServiceRuntime* service_runtime, 195 ServiceRuntime* service_runtime,
125 pp::CompletionCallback init_done_cb, 196 pp::CompletionCallback init_done_cb,
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
193 // and invoke StreamAsFile with a completion callback that invokes 264 // and invoke StreamAsFile with a completion callback that invokes
194 // GetPOSIXFileDesc. 265 // GetPOSIXFileDesc.
195 bool PluginReverseInterface::OpenManifestEntry(nacl::string url_key, 266 bool PluginReverseInterface::OpenManifestEntry(nacl::string url_key,
196 struct NaClFileInfo* info) { 267 struct NaClFileInfo* info) {
197 bool op_complete = false; // NB: mu_ and cv_ also controls access to this! 268 bool op_complete = false; // NB: mu_ and cv_ also controls access to this!
198 // The to_open object is owned by the weak ref callback. Because this function 269 // The to_open object is owned by the weak ref callback. Because this function
199 // waits for the callback to finish, the to_open object will be deallocated on 270 // waits for the callback to finish, the to_open object will be deallocated on
200 // the main thread before this function can return. The pointers it contains 271 // the main thread before this function can return. The pointers it contains
201 // to stack variables will not leak. 272 // to stack variables will not leak.
202 OpenManifestEntryResource* to_open = 273 OpenManifestEntryResource* to_open =
203 new OpenManifestEntryResource(url_key, info, &op_complete); 274 new OpenManifestEntryResource(url_key, info, &op_complete,
275 pp::CompletionCallback());
204 CHECK(to_open != NULL); 276 CHECK(to_open != NULL);
205 NaClLog(4, "PluginReverseInterface::OpenManifestEntry: %s\n", 277 NaClLog(4, "PluginReverseInterface::OpenManifestEntry: %s\n",
206 url_key.c_str()); 278 url_key.c_str());
207 // This assumes we are not on the main thread. If false, we deadlock. 279 // This assumes we are not on the main thread. If false, we deadlock.
208 plugin::WeakRefCallOnMainThread( 280 plugin::WeakRefCallOnMainThread(
209 anchor_, 281 anchor_,
210 0, 282 0,
211 this, 283 this,
212 &plugin::PluginReverseInterface::OpenManifestEntry_MainThreadContinuation, 284 &plugin::PluginReverseInterface::OpenManifestEntry_MainThreadContinuation,
213 to_open); 285 to_open);
(...skipping 29 matching lines...) Expand all
243 if (info->desc == -1) { 315 if (info->desc == -1) {
244 // TODO(bsy,ncbray): what else should we do with the error? This 316 // TODO(bsy,ncbray): what else should we do with the error? This
245 // is a runtime error that may simply be a programming error in 317 // is a runtime error that may simply be a programming error in
246 // the untrusted code, or it may be something else wrong w/ the 318 // the untrusted code, or it may be something else wrong w/ the
247 // manifest. 319 // manifest.
248 NaClLog(4, "OpenManifestEntry: failed for key %s", url_key.c_str()); 320 NaClLog(4, "OpenManifestEntry: failed for key %s", url_key.c_str());
249 } 321 }
250 return true; 322 return true;
251 } 323 }
252 324
325 void PluginReverseInterface::OpenManifestEntryAsync(
326 const nacl::string& entry_key,
327 struct NaClFileInfo* info,
328 const pp::CompletionCallback& callback) {
329 bool op_complete = false;
330 OpenManifestEntryResource to_open(
331 entry_key, info, &op_complete, callback);
332 OpenManifestEntry_MainThreadContinuation(&to_open, PP_OK);
333 }
334
253 // Transfer point from OpenManifestEntry() which runs on the main thread 335 // Transfer point from OpenManifestEntry() which runs on the main thread
254 // (Some PPAPI actions -- like StreamAsFile -- can only run on the main thread). 336 // (Some PPAPI actions -- like StreamAsFile -- can only run on the main thread).
255 // OpenManifestEntry() is waiting on a condvar for this continuation to 337 // OpenManifestEntry() is waiting on a condvar for this continuation to
256 // complete. We Broadcast and awaken OpenManifestEntry() whenever we are done 338 // complete. We Broadcast and awaken OpenManifestEntry() whenever we are done
257 // either here, or in a later MainThreadContinuation step, if there are 339 // either here, or in a later MainThreadContinuation step, if there are
258 // multiple steps. 340 // multiple steps.
259 void PluginReverseInterface::OpenManifestEntry_MainThreadContinuation( 341 void PluginReverseInterface::OpenManifestEntry_MainThreadContinuation(
260 OpenManifestEntryResource* p, 342 OpenManifestEntryResource* p,
261 int32_t err) { 343 int32_t err) {
262 UNREFERENCED_PARAMETER(err); 344 UNREFERENCED_PARAMETER(err);
263 // CallOnMainThread continuations always called with err == PP_OK. 345 // CallOnMainThread continuations always called with err == PP_OK.
264 346
265 NaClLog(4, "Entered OpenManifestEntry_MainThreadContinuation\n"); 347 NaClLog(4, "Entered OpenManifestEntry_MainThreadContinuation\n");
266 348
267 std::string mapped_url; 349 std::string mapped_url;
268 PP_PNaClOptions pnacl_options = {PP_FALSE, PP_FALSE, 2}; 350 PP_PNaClOptions pnacl_options = {PP_FALSE, PP_FALSE, 2};
269 ErrorInfo error_info; 351 ErrorInfo error_info;
270 if (!manifest_->ResolveKey(p->url, &mapped_url, 352 if (!manifest_->ResolveKey(p->url, &mapped_url,
271 &pnacl_options, &error_info)) { 353 &pnacl_options, &error_info)) {
272 NaClLog(4, "OpenManifestEntry_MainThreadContinuation: ResolveKey failed\n"); 354 NaClLog(4, "OpenManifestEntry_MainThreadContinuation: ResolveKey failed\n");
273 NaClLog(4, 355 NaClLog(4,
274 "Error code %d, string %s\n", 356 "Error code %d, string %s\n",
275 error_info.error_code(), 357 error_info.error_code(),
276 error_info.message().c_str()); 358 error_info.message().c_str());
277 // Failed, and error_info has the details on what happened. Wake 359 // Failed, and error_info has the details on what happened. Wake
278 // up requesting thread -- we are done. 360 // up requesting thread -- we are done.
279 nacl::MutexLocker take(&mu_); 361 {
280 *p->op_complete_ptr = true; // done... 362 nacl::MutexLocker take(&mu_);
281 p->file_info->desc = -1; // but failed. 363 *p->op_complete_ptr = true; // done...
282 NaClXCondVarBroadcast(&cv_); 364 p->file_info->desc = -1; // but failed.
365 NaClXCondVarBroadcast(&cv_);
366 }
367 if (p->callback.pp_completion_callback().func)
368 p->callback.RunAndClear(PP_OK);
283 return; 369 return;
284 } 370 }
285 NaClLog(4, 371 NaClLog(4,
286 "OpenManifestEntry_MainThreadContinuation: " 372 "OpenManifestEntry_MainThreadContinuation: "
287 "ResolveKey: %s -> %s (pnacl_translate(%d))\n", 373 "ResolveKey: %s -> %s (pnacl_translate(%d))\n",
288 p->url.c_str(), mapped_url.c_str(), pnacl_options.translate); 374 p->url.c_str(), mapped_url.c_str(), pnacl_options.translate);
289 375
290 if (pnacl_options.translate) { 376 if (pnacl_options.translate) {
291 // Requires PNaCl translation, but that's not supported. 377 // Requires PNaCl translation, but that's not supported.
292 NaClLog(4, 378 NaClLog(4,
293 "OpenManifestEntry_MainThreadContinuation: " 379 "OpenManifestEntry_MainThreadContinuation: "
294 "Requires PNaCl translation -- not supported\n"); 380 "Requires PNaCl translation -- not supported\n");
295 nacl::MutexLocker take(&mu_); 381 {
296 *p->op_complete_ptr = true; // done... 382 nacl::MutexLocker take(&mu_);
297 p->file_info->desc = -1; // but failed. 383 *p->op_complete_ptr = true; // done...
298 NaClXCondVarBroadcast(&cv_); 384 p->file_info->desc = -1; // but failed.
385 NaClXCondVarBroadcast(&cv_);
386 }
387 if (p->callback.pp_completion_callback().func)
388 p->callback.RunAndClear(PP_OK);
299 return; 389 return;
300 } 390 }
301 391
302 if (PnaclUrls::IsPnaclComponent(mapped_url)) { 392 if (PnaclUrls::IsPnaclComponent(mapped_url)) {
303 // Special PNaCl support files, that are installed on the 393 // Special PNaCl support files, that are installed on the
304 // user machine. 394 // user machine.
305 int32_t fd = PnaclResources::GetPnaclFD( 395 int32_t fd = PnaclResources::GetPnaclFD(
306 plugin_, 396 plugin_,
307 PnaclUrls::PnaclComponentURLToFilename(mapped_url).c_str()); 397 PnaclUrls::PnaclComponentURLToFilename(mapped_url).c_str());
308 if (fd < 0) { 398 if (fd < 0) {
309 // We checked earlier if the pnacl component wasn't installed 399 // We checked earlier if the pnacl component wasn't installed
310 // yet, so this shouldn't happen. At this point, we can't do much 400 // yet, so this shouldn't happen. At this point, we can't do much
311 // anymore, so just continue with an invalid fd. 401 // anymore, so just continue with an invalid fd.
312 NaClLog(4, 402 NaClLog(4,
313 "OpenManifestEntry_MainThreadContinuation: " 403 "OpenManifestEntry_MainThreadContinuation: "
314 "GetReadonlyPnaclFd failed\n"); 404 "GetReadonlyPnaclFd failed\n");
315 } 405 }
316 nacl::MutexLocker take(&mu_); 406 {
317 *p->op_complete_ptr = true; // done! 407 nacl::MutexLocker take(&mu_);
318 // TODO(ncbray): enable the fast loading and validation paths for this 408 *p->op_complete_ptr = true; // done!
319 // type of file. 409 // TODO(ncbray): enable the fast loading and validation paths for this
320 p->file_info->desc = fd; 410 // type of file.
321 NaClXCondVarBroadcast(&cv_); 411 p->file_info->desc = fd;
412 NaClXCondVarBroadcast(&cv_);
413 }
322 NaClLog(4, 414 NaClLog(4,
323 "OpenManifestEntry_MainThreadContinuation: GetPnaclFd okay\n"); 415 "OpenManifestEntry_MainThreadContinuation: GetPnaclFd okay\n");
416 if (p->callback.pp_completion_callback().func)
417 p->callback.RunAndClear(PP_OK);
324 return; 418 return;
325 } 419 }
326 420
327 // Hereafter, normal files. 421 // Hereafter, normal files.
328 422
329 // Because p is owned by the callback of this invocation, so it is necessary 423 // Because p is owned by the callback of this invocation, so it is necessary
330 // to create another instance. 424 // to create another instance.
331 OpenManifestEntryResource* open_cont = new OpenManifestEntryResource(*p); 425 OpenManifestEntryResource* open_cont = new OpenManifestEntryResource(*p);
332 open_cont->url = mapped_url; 426 open_cont->url = mapped_url;
427 // Callback is now delegated from p to open_cont. So, here we manually clear
428 // complete callback.
429 p->callback = pp::CompletionCallback();
333 pp::CompletionCallback stream_cc = WeakRefNewCallback( 430 pp::CompletionCallback stream_cc = WeakRefNewCallback(
334 anchor_, 431 anchor_,
335 this, 432 this,
336 &PluginReverseInterface::StreamAsFile_MainThreadContinuation, 433 &PluginReverseInterface::StreamAsFile_MainThreadContinuation,
337 open_cont); 434 open_cont);
338 435
339 if (!plugin_->StreamAsFile(mapped_url, stream_cc)) { 436 if (!plugin_->StreamAsFile(mapped_url, stream_cc)) {
340 NaClLog(4, 437 NaClLog(4,
341 "OpenManifestEntry_MainThreadContinuation: " 438 "OpenManifestEntry_MainThreadContinuation: "
342 "StreamAsFile failed\n"); 439 "StreamAsFile failed\n");
343 // Here, StreamAsFile is failed and stream_cc is not called. 440 // Here, StreamAsFile is failed and stream_cc is not called.
344 // However, open_cont will be released only by the invocation. 441 // However, open_cont will be released only by the invocation.
345 // So, we manually call it here with error. 442 // So, we manually call it here with error.
346 stream_cc.Run(PP_ERROR_FAILED); 443 stream_cc.Run(PP_ERROR_FAILED);
347 return; 444 return;
348 } 445 }
349 446
350 NaClLog(4, "OpenManifestEntry_MainThreadContinuation: StreamAsFile okay\n"); 447 NaClLog(4, "OpenManifestEntry_MainThreadContinuation: StreamAsFile okay\n");
351 // p is deleted automatically 448 // p is deleted automatically
352 } 449 }
353 450
354 void PluginReverseInterface::StreamAsFile_MainThreadContinuation( 451 void PluginReverseInterface::StreamAsFile_MainThreadContinuation(
355 OpenManifestEntryResource* p, 452 OpenManifestEntryResource* p,
356 int32_t result) { 453 int32_t result) {
357 NaClLog(4, 454 NaClLog(4,
358 "Entered StreamAsFile_MainThreadContinuation\n"); 455 "Entered StreamAsFile_MainThreadContinuation\n");
359 456
360 nacl::MutexLocker take(&mu_); 457 {
361 if (result == PP_OK) { 458 nacl::MutexLocker take(&mu_);
362 NaClLog(4, "StreamAsFile_MainThreadContinuation: GetFileInfo(%s)\n", 459 if (result == PP_OK) {
363 p->url.c_str()); 460 NaClLog(4, "StreamAsFile_MainThreadContinuation: GetFileInfo(%s)\n",
364 *p->file_info = plugin_->GetFileInfo(p->url); 461 p->url.c_str());
462 *p->file_info = plugin_->GetFileInfo(p->url);
365 463
366 NaClLog(4, 464 NaClLog(4,
367 "StreamAsFile_MainThreadContinuation: PP_OK, desc %d\n", 465 "StreamAsFile_MainThreadContinuation: PP_OK, desc %d\n",
368 p->file_info->desc); 466 p->file_info->desc);
369 } else { 467 } else {
370 NaClLog(4, 468 NaClLog(
371 "StreamAsFile_MainThreadContinuation: !PP_OK, setting desc -1\n"); 469 4,
372 p->file_info->desc = -1; 470 "StreamAsFile_MainThreadContinuation: !PP_OK, setting desc -1\n");
471 p->file_info->desc = -1;
472 }
473 *p->op_complete_ptr = true;
474 NaClXCondVarBroadcast(&cv_);
373 } 475 }
374 *p->op_complete_ptr = true; 476 if (p->callback.pp_completion_callback().func)
375 NaClXCondVarBroadcast(&cv_); 477 p->callback.RunAndClear(PP_OK);
376 } 478 }
377 479
378 bool PluginReverseInterface::CloseManifestEntry(int32_t desc) { 480 bool PluginReverseInterface::CloseManifestEntry(int32_t desc) {
379 bool op_complete = false; 481 bool op_complete = false;
380 bool op_result; 482 bool op_result;
381 CloseManifestEntryResource* to_close = 483 CloseManifestEntryResource* to_close =
382 new CloseManifestEntryResource(desc, &op_complete, &op_result); 484 new CloseManifestEntryResource(desc, &op_complete, &op_result);
383 485
384 plugin::WeakRefCallOnMainThread( 486 plugin::WeakRefCallOnMainThread(
385 anchor_, 487 anchor_,
(...skipping 415 matching lines...) Expand 10 before | Expand all | Expand 10 after
801 903
802 nacl::string ServiceRuntime::GetCrashLogOutput() { 904 nacl::string ServiceRuntime::GetCrashLogOutput() {
803 if (NULL != subprocess_.get()) { 905 if (NULL != subprocess_.get()) {
804 return subprocess_->GetCrashLogOutput(); 906 return subprocess_->GetCrashLogOutput();
805 } else { 907 } else {
806 return std::string(); 908 return std::string();
807 } 909 }
808 } 910 }
809 911
810 } // namespace plugin 912 } // namespace plugin
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698