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

Side by Side Diff: runtime/bin/file_system_watcher_macos.cc

Issue 2480793002: clang-format runtime/bin (Closed)
Patch Set: Created 4 years, 1 month 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
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 #if !defined(DART_IO_DISABLED) 5 #if !defined(DART_IO_DISABLED)
6 6
7 #include "platform/globals.h" 7 #include "platform/globals.h"
8 #if defined(TARGET_OS_MACOS) 8 #if defined(TARGET_OS_MACOS)
9 9
10 #include "bin/file_system_watcher.h" 10 #include "bin/file_system_watcher.h"
11 11
12 #if !TARGET_OS_IOS 12 #if !TARGET_OS_IOS
13 13
14 #include <errno.h> // NOLINT 14 #include <errno.h> // NOLINT
15 #include <fcntl.h> // NOLINT 15 #include <fcntl.h> // NOLINT
16 #include <unistd.h> // NOLINT 16 #include <unistd.h> // NOLINT
17 #include <CoreServices/CoreServices.h> // NOLINT 17 #include <CoreServices/CoreServices.h> // NOLINT
18 18
19 #include "bin/eventhandler.h" 19 #include "bin/eventhandler.h"
20 #include "bin/fdutils.h" 20 #include "bin/fdutils.h"
21 #include "bin/file.h" 21 #include "bin/file.h"
22 #include "bin/socket.h" 22 #include "bin/socket.h"
23 #include "bin/thread.h" 23 #include "bin/thread.h"
24 #include "platform/signal_blocker.h" 24 #include "platform/signal_blocker.h"
25 25
26 #ifndef MAC_OS_X_VERSION_10_7 26 #ifndef MAC_OS_X_VERSION_10_7
27 enum { 27 enum { kFSEventStreamCreateFlagFileEvents = 0x00000010 };
28 kFSEventStreamCreateFlagFileEvents = 0x00000010
29 };
30 enum { 28 enum {
31 kFSEventStreamEventFlagItemCreated = 0x00000100, 29 kFSEventStreamEventFlagItemCreated = 0x00000100,
32 kFSEventStreamEventFlagItemRemoved = 0x00000200, 30 kFSEventStreamEventFlagItemRemoved = 0x00000200,
33 kFSEventStreamEventFlagItemInodeMetaMod = 0x00000400, 31 kFSEventStreamEventFlagItemInodeMetaMod = 0x00000400,
34 kFSEventStreamEventFlagItemRenamed = 0x00000800, 32 kFSEventStreamEventFlagItemRenamed = 0x00000800,
35 kFSEventStreamEventFlagItemModified = 0x00001000, 33 kFSEventStreamEventFlagItemModified = 0x00001000,
36 kFSEventStreamEventFlagItemFinderInfoMod = 0x00002000, 34 kFSEventStreamEventFlagItemFinderInfoMod = 0x00002000,
37 kFSEventStreamEventFlagItemChangeOwner = 0x00004000, 35 kFSEventStreamEventFlagItemChangeOwner = 0x00004000,
38 kFSEventStreamEventFlagItemXattrMod = 0x00008000, 36 kFSEventStreamEventFlagItemXattrMod = 0x00008000,
39 kFSEventStreamEventFlagItemIsFile = 0x00010000, 37 kFSEventStreamEventFlagItemIsFile = 0x00010000,
(...skipping 12 matching lines...) Expand all
52 char path[PATH_MAX]; 50 char path[PATH_MAX];
53 } data; 51 } data;
54 uint8_t bytes[PATH_MAX + 8]; 52 uint8_t bytes[PATH_MAX + 8];
55 }; 53 };
56 54
57 55
58 class FSEventsWatcher { 56 class FSEventsWatcher {
59 public: 57 public:
60 class Node { 58 class Node {
61 public: 59 public:
62 Node(FSEventsWatcher* watcher, char* base_path, int read_fd, 60 Node(FSEventsWatcher* watcher,
63 int write_fd, bool recursive) 61 char* base_path,
62 int read_fd,
63 int write_fd,
64 bool recursive)
64 : watcher_(watcher), 65 : watcher_(watcher),
65 ready_(false), 66 ready_(false),
66 base_path_length_(strlen(base_path)), 67 base_path_length_(strlen(base_path)),
67 path_ref_(CFStringCreateWithCString( 68 path_ref_(CFStringCreateWithCString(NULL,
68 NULL, base_path, kCFStringEncodingUTF8)), 69 base_path,
70 kCFStringEncodingUTF8)),
69 read_fd_(read_fd), 71 read_fd_(read_fd),
70 write_fd_(write_fd), 72 write_fd_(write_fd),
71 recursive_(recursive), 73 recursive_(recursive),
72 ref_(NULL) { 74 ref_(NULL) {
73 Start(); 75 Start();
74 } 76 }
75 77
76 ~Node() { 78 ~Node() {
77 Stop(); 79 Stop();
78 VOID_TEMP_FAILURE_RETRY(close(write_fd_)); 80 VOID_TEMP_FAILURE_RETRY(close(write_fd_));
79 CFRelease(path_ref_); 81 CFRelease(path_ref_);
80 } 82 }
81 83
82 void set_ref(FSEventStreamRef ref) { 84 void set_ref(FSEventStreamRef ref) { ref_ = ref; }
83 ref_ = ref;
84 }
85 85
86 void Start() { 86 void Start() {
87 // Schedule StartCallback to be executed in the RunLoop. 87 // Schedule StartCallback to be executed in the RunLoop.
88 CFRunLoopTimerContext context; 88 CFRunLoopTimerContext context;
89 memset(&context, 0, sizeof(context)); 89 memset(&context, 0, sizeof(context));
90 context.info = this; 90 context.info = this;
91 CFRunLoopTimerRef timer = CFRunLoopTimerCreate( 91 CFRunLoopTimerRef timer =
92 NULL, 0, 0, 0, 0, Node::StartCallback, &context); 92 CFRunLoopTimerCreate(NULL, 0, 0, 0, 0, Node::StartCallback, &context);
93 CFRunLoopAddTimer(watcher_->run_loop_, timer, kCFRunLoopCommonModes); 93 CFRunLoopAddTimer(watcher_->run_loop_, timer, kCFRunLoopCommonModes);
94 CFRelease(timer); 94 CFRelease(timer);
95 watcher_->monitor_.Enter(); 95 watcher_->monitor_.Enter();
96 while (!ready_) { 96 while (!ready_) {
97 watcher_->monitor_.Wait(Monitor::kNoTimeout); 97 watcher_->monitor_.Wait(Monitor::kNoTimeout);
98 } 98 }
99 watcher_->monitor_.Exit(); 99 watcher_->monitor_.Exit();
100 } 100 }
101 101
102 static void StartCallback(CFRunLoopTimerRef timer, void* info) { 102 static void StartCallback(CFRunLoopTimerRef timer, void* info) {
103 Node* node = reinterpret_cast<Node*>(info); 103 Node* node = reinterpret_cast<Node*>(info);
104 ASSERT(Thread::Compare(node->watcher_->threadId_, 104 ASSERT(Thread::Compare(node->watcher_->threadId_,
105 Thread::GetCurrentThreadId())); 105 Thread::GetCurrentThreadId()));
106 FSEventStreamContext context; 106 FSEventStreamContext context;
107 memset(&context, 0, sizeof(context)); 107 memset(&context, 0, sizeof(context));
108 context.info = reinterpret_cast<void*>(node); 108 context.info = reinterpret_cast<void*>(node);
109 CFArrayRef array = CFArrayCreate( 109 CFArrayRef array = CFArrayCreate(
110 NULL, reinterpret_cast<const void**>(&node->path_ref_), 1, NULL); 110 NULL, reinterpret_cast<const void**>(&node->path_ref_), 1, NULL);
111 FSEventStreamRef ref = FSEventStreamCreate( 111 FSEventStreamRef ref = FSEventStreamCreate(
112 NULL, 112 NULL, Callback, &context, array, kFSEventStreamEventIdSinceNow, 0.10,
113 Callback,
114 &context,
115 array,
116 kFSEventStreamEventIdSinceNow,
117 0.10,
118 kFSEventStreamCreateFlagFileEvents); 113 kFSEventStreamCreateFlagFileEvents);
119 CFRelease(array); 114 CFRelease(array);
120 115
121 node->set_ref(ref); 116 node->set_ref(ref);
122 117
123 FSEventStreamScheduleWithRunLoop( 118 FSEventStreamScheduleWithRunLoop(node->ref_, node->watcher_->run_loop_,
124 node->ref_, 119 kCFRunLoopDefaultMode);
125 node->watcher_->run_loop_,
126 kCFRunLoopDefaultMode);
127 120
128 FSEventStreamStart(node->ref_); 121 FSEventStreamStart(node->ref_);
129 FSEventStreamFlushSync(node->ref_); 122 FSEventStreamFlushSync(node->ref_);
130 123
131 node->watcher_->monitor_.Enter(); 124 node->watcher_->monitor_.Enter();
132 node->ready_ = true; 125 node->ready_ = true;
133 node->watcher_->monitor_.Notify(); 126 node->watcher_->monitor_.Notify();
134 node->watcher_->monitor_.Exit(); 127 node->watcher_->monitor_.Exit();
135 } 128 }
136 129
137 void Stop() { 130 void Stop() {
138 // Schedule StopCallback to be executed in the RunLoop. 131 // Schedule StopCallback to be executed in the RunLoop.
139 ASSERT(ready_); 132 ASSERT(ready_);
140 CFRunLoopTimerContext context; 133 CFRunLoopTimerContext context;
141 memset(&context, 0, sizeof(context)); 134 memset(&context, 0, sizeof(context));
142 context.info = this; 135 context.info = this;
143 CFRunLoopTimerRef timer = CFRunLoopTimerCreate( 136 CFRunLoopTimerRef timer =
144 NULL, 0, 0, 0, 0, StopCallback, &context); 137 CFRunLoopTimerCreate(NULL, 0, 0, 0, 0, StopCallback, &context);
145 CFRunLoopAddTimer(watcher_->run_loop_, timer, kCFRunLoopCommonModes); 138 CFRunLoopAddTimer(watcher_->run_loop_, timer, kCFRunLoopCommonModes);
146 CFRelease(timer); 139 CFRelease(timer);
147 watcher_->monitor_.Enter(); 140 watcher_->monitor_.Enter();
148 while (ready_) { 141 while (ready_) {
149 watcher_->monitor_.Wait(Monitor::kNoTimeout); 142 watcher_->monitor_.Wait(Monitor::kNoTimeout);
150 } 143 }
151 watcher_->monitor_.Exit(); 144 watcher_->monitor_.Exit();
152 } 145 }
153 146
154 static void StopCallback(CFRunLoopTimerRef timer, void* info) { 147 static void StopCallback(CFRunLoopTimerRef timer, void* info) {
(...skipping 23 matching lines...) Expand all
178 CFStringRef path_ref_; 171 CFStringRef path_ref_;
179 int read_fd_; 172 int read_fd_;
180 int write_fd_; 173 int write_fd_;
181 bool recursive_; 174 bool recursive_;
182 FSEventStreamRef ref_; 175 FSEventStreamRef ref_;
183 176
184 DISALLOW_COPY_AND_ASSIGN(Node); 177 DISALLOW_COPY_AND_ASSIGN(Node);
185 }; 178 };
186 179
187 180
188 FSEventsWatcher() : run_loop_(0) { 181 FSEventsWatcher() : run_loop_(0) { Start(); }
189 Start();
190 }
191 182
192 void Start() { 183 void Start() {
193 Thread::Start(Run, reinterpret_cast<uword>(this)); 184 Thread::Start(Run, reinterpret_cast<uword>(this));
194 monitor_.Enter(); 185 monitor_.Enter();
195 while (run_loop_ == NULL) { 186 while (run_loop_ == NULL) {
196 monitor_.Wait(Monitor::kNoTimeout); 187 monitor_.Wait(Monitor::kNoTimeout);
197 } 188 }
198 monitor_.Exit(); 189 monitor_.Exit();
199 } 190 }
200 191
201 static void Run(uword arg) { 192 static void Run(uword arg) {
202 FSEventsWatcher* watcher = reinterpret_cast<FSEventsWatcher*>(arg); 193 FSEventsWatcher* watcher = reinterpret_cast<FSEventsWatcher*>(arg);
203 // Only checked in debug mode. 194 // Only checked in debug mode.
204 watcher->threadId_ = Thread::GetCurrentThreadId(); 195 watcher->threadId_ = Thread::GetCurrentThreadId();
205 watcher->run_loop_ = CFRunLoopGetCurrent(); 196 watcher->run_loop_ = CFRunLoopGetCurrent();
206 CFRetain(watcher->run_loop_); 197 CFRetain(watcher->run_loop_);
207 198
208 // Notify, as the run-loop is set. 199 // Notify, as the run-loop is set.
209 watcher->monitor().Enter(); 200 watcher->monitor().Enter();
210 watcher->monitor().Notify(); 201 watcher->monitor().Notify();
211 watcher->monitor().Exit(); 202 watcher->monitor().Exit();
212 203
213 CFRunLoopTimerRef timer = CFRunLoopTimerCreate( 204 CFRunLoopTimerRef timer = CFRunLoopTimerCreate(
214 NULL, 205 NULL, CFAbsoluteTimeGetCurrent() + 1, 1, 0, 0, TimerCallback, NULL);
215 CFAbsoluteTimeGetCurrent() + 1,
216 1,
217 0,
218 0,
219 TimerCallback,
220 NULL);
221 CFRunLoopAddTimer(watcher->run_loop_, timer, kCFRunLoopCommonModes); 206 CFRunLoopAddTimer(watcher->run_loop_, timer, kCFRunLoopCommonModes);
222 CFRelease(timer); 207 CFRelease(timer);
223 208
224 CFRunLoopRun(); 209 CFRunLoopRun();
225 210
226 CFRelease(watcher->run_loop_); 211 CFRelease(watcher->run_loop_);
227 watcher->monitor_.Enter(); 212 watcher->monitor_.Enter();
228 watcher->run_loop_ = NULL; 213 watcher->run_loop_ = NULL;
229 watcher->monitor_.Notify(); 214 watcher->monitor_.Notify();
230 watcher->monitor_.Exit(); 215 watcher->monitor_.Exit();
231 } 216 }
232 217
233 void Stop() { 218 void Stop() {
234 // Schedule StopCallback to be executed in the RunLoop. 219 // Schedule StopCallback to be executed in the RunLoop.
235 CFRunLoopTimerContext context; 220 CFRunLoopTimerContext context;
236 memset(&context, 0, sizeof(context)); 221 memset(&context, 0, sizeof(context));
237 context.info = this; 222 context.info = this;
238 CFRunLoopTimerRef timer = CFRunLoopTimerCreate( 223 CFRunLoopTimerRef timer =
239 NULL, 0, 0, 0, 0, StopCallback, &context); 224 CFRunLoopTimerCreate(NULL, 0, 0, 0, 0, StopCallback, &context);
240 CFRunLoopAddTimer(run_loop_, timer, kCFRunLoopCommonModes); 225 CFRunLoopAddTimer(run_loop_, timer, kCFRunLoopCommonModes);
241 CFRelease(timer); 226 CFRelease(timer);
242 monitor_.Enter(); 227 monitor_.Enter();
243 while (run_loop_ != NULL) { 228 while (run_loop_ != NULL) {
244 monitor_.Wait(Monitor::kNoTimeout); 229 monitor_.Wait(Monitor::kNoTimeout);
245 } 230 }
246 monitor_.Exit(); 231 monitor_.Exit();
247 } 232 }
248 233
249 static void StopCallback(CFRunLoopTimerRef timer, void* info) { 234 static void StopCallback(CFRunLoopTimerRef timer, void* info) {
250 FSEventsWatcher* watcher = reinterpret_cast<FSEventsWatcher*>(info); 235 FSEventsWatcher* watcher = reinterpret_cast<FSEventsWatcher*>(info);
251 ASSERT(Thread::Compare(watcher->threadId_, 236 ASSERT(Thread::Compare(watcher->threadId_, Thread::GetCurrentThreadId()));
252 Thread::GetCurrentThreadId()));
253 CFRunLoopStop(watcher->run_loop_); 237 CFRunLoopStop(watcher->run_loop_);
254 } 238 }
255 239
256 ~FSEventsWatcher() { 240 ~FSEventsWatcher() { Stop(); }
257 Stop();
258 }
259 241
260 Monitor& monitor() { return monitor_; } 242 Monitor& monitor() { return monitor_; }
261 243
262 bool has_run_loop() const { return run_loop_ != NULL; } 244 bool has_run_loop() const { return run_loop_ != NULL; }
263 245
264 static void TimerCallback(CFRunLoopTimerRef timer, void* context) { 246 static void TimerCallback(CFRunLoopTimerRef timer, void* context) {
265 // Dummy callback to keep RunLoop alive. 247 // Dummy callback to keep RunLoop alive.
266 } 248 }
267 249
268 Node* AddPath(const char* path, int events, bool recursive) { 250 Node* AddPath(const char* path, int events, bool recursive) {
(...skipping 17 matching lines...) Expand all
286 const FSEventStreamEventId event_ids[]) { 268 const FSEventStreamEventId event_ids[]) {
287 Node* node = reinterpret_cast<Node*>(client); 269 Node* node = reinterpret_cast<Node*>(client);
288 ASSERT(Thread::Compare(node->watcher()->threadId_, 270 ASSERT(Thread::Compare(node->watcher()->threadId_,
289 Thread::GetCurrentThreadId())); 271 Thread::GetCurrentThreadId()));
290 // `ready` is set on same thread as this callback is invoked, so we don't 272 // `ready` is set on same thread as this callback is invoked, so we don't
291 // need to lock here. 273 // need to lock here.
292 if (!node->ready()) { 274 if (!node->ready()) {
293 return; 275 return;
294 } 276 }
295 for (size_t i = 0; i < num_events; i++) { 277 for (size_t i = 0; i < num_events; i++) {
296 char *path = reinterpret_cast<char**>(event_paths)[i]; 278 char* path = reinterpret_cast<char**>(event_paths)[i];
297 FSEvent event; 279 FSEvent event;
298 event.data.exists = File::GetType(path, false) != File::kDoesNotExist; 280 event.data.exists = File::GetType(path, false) != File::kDoesNotExist;
299 path += node->base_path_length(); 281 path += node->base_path_length();
300 // If path is longer the base, skip next character ('/'). 282 // If path is longer the base, skip next character ('/').
301 if (path[0] != '\0') { 283 if (path[0] != '\0') {
302 path += 1; 284 path += 1;
303 } 285 }
304 if (!node->recursive() && (strstr(path, "/") != NULL)) { 286 if (!node->recursive() && (strstr(path, "/") != NULL)) {
305 continue; 287 continue;
306 } 288 }
307 event.data.flags = event_flags[i]; 289 event.data.flags = event_flags[i];
308 memmove(event.data.path, path, strlen(path) + 1); 290 memmove(event.data.path, path, strlen(path) + 1);
309 write(node->write_fd(), event.bytes, sizeof(event)); 291 write(node->write_fd(), event.bytes, sizeof(event));
310 } 292 }
311 } 293 }
312 294
313 Monitor monitor_; 295 Monitor monitor_;
314 CFRunLoopRef run_loop_; 296 CFRunLoopRef run_loop_;
315 ThreadId threadId_; 297 ThreadId threadId_;
316 298
317 DISALLOW_COPY_AND_ASSIGN(FSEventsWatcher); 299 DISALLOW_COPY_AND_ASSIGN(FSEventsWatcher);
318 }; 300 };
319 301
320 302
321 #define kCFCoreFoundationVersionNumber10_7 635.00 303 #define kCFCoreFoundationVersionNumber10_7 635.00
322 bool FileSystemWatcher::IsSupported() { 304 bool FileSystemWatcher::IsSupported() {
323 return kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber10_7; 305 return kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber10_7;
324 } 306 }
325 307
326 308
327 intptr_t FileSystemWatcher::Init() { 309 intptr_t FileSystemWatcher::Init() {
328 return reinterpret_cast<intptr_t>(new FSEventsWatcher()); 310 return reinterpret_cast<intptr_t>(new FSEventsWatcher());
329 } 311 }
330 312
331 313
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after
395 if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) { 377 if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) {
396 if (path_len == 0) { 378 if (path_len == 0) {
397 // The removed path is the path being watched. 379 // The removed path is the path being watched.
398 mask |= kDeleteSelf; 380 mask |= kDeleteSelf;
399 } else { 381 } else {
400 mask |= kDelete; 382 mask |= kDelete;
401 } 383 }
402 } 384 }
403 Dart_ListSetAt(event, 0, Dart_NewInteger(mask)); 385 Dart_ListSetAt(event, 0, Dart_NewInteger(mask));
404 Dart_ListSetAt(event, 1, Dart_NewInteger(1)); 386 Dart_ListSetAt(event, 1, Dart_NewInteger(1));
405 Dart_ListSetAt(event, 2, Dart_NewStringFromUTF8( 387 Dart_ListSetAt(event, 2,
406 reinterpret_cast<uint8_t*>(e.data.path), path_len)); 388 Dart_NewStringFromUTF8(
389 reinterpret_cast<uint8_t*>(e.data.path), path_len));
407 Dart_ListSetAt(event, 3, Dart_NewBoolean(true)); 390 Dart_ListSetAt(event, 3, Dart_NewBoolean(true));
408 Dart_ListSetAt(event, 4, Dart_NewInteger(path_id)); 391 Dart_ListSetAt(event, 4, Dart_NewInteger(path_id));
409 Dart_ListSetAt(events, i, event); 392 Dart_ListSetAt(events, i, event);
410 } 393 }
411 return events; 394 return events;
412 } 395 }
413 396
414 } // namespace bin 397 } // namespace bin
415 } // namespace dart 398 } // namespace dart
416 399
(...skipping 11 matching lines...) Expand all
428 intptr_t FileSystemWatcher::GetSocketId(intptr_t id, intptr_t path_id) { 411 intptr_t FileSystemWatcher::GetSocketId(intptr_t id, intptr_t path_id) {
429 return -1; 412 return -1;
430 } 413 }
431 414
432 415
433 bool FileSystemWatcher::IsSupported() { 416 bool FileSystemWatcher::IsSupported() {
434 return false; 417 return false;
435 } 418 }
436 419
437 420
438 void FileSystemWatcher::UnwatchPath(intptr_t id, intptr_t path_id) { 421 void FileSystemWatcher::UnwatchPath(intptr_t id, intptr_t path_id) {}
439 }
440 422
441 423
442 intptr_t FileSystemWatcher::Init() { 424 intptr_t FileSystemWatcher::Init() {
443 return -1; 425 return -1;
444 } 426 }
445 427
446 428
447 void FileSystemWatcher::Close(intptr_t id) { 429 void FileSystemWatcher::Close(intptr_t id) {}
448 }
449 430
450 431
451 intptr_t FileSystemWatcher::WatchPath(intptr_t id, 432 intptr_t FileSystemWatcher::WatchPath(intptr_t id,
452 const char* path, 433 const char* path,
453 int events, 434 int events,
454 bool recursive) { 435 bool recursive) {
455 return -1; 436 return -1;
456 } 437 }
457 438
458 } // namespace bin 439 } // namespace bin
459 } // namespace dart 440 } // namespace dart
460 441
461 #endif // !TARGET_OS_IOS 442 #endif // !TARGET_OS_IOS
462 #endif // defined(TARGET_OS_MACOS) 443 #endif // defined(TARGET_OS_MACOS)
463 444
464 #endif // !defined(DART_IO_DISABLED) 445 #endif // !defined(DART_IO_DISABLED)
OLDNEW
« no previous file with comments | « runtime/bin/file_system_watcher_linux.cc ('k') | runtime/bin/file_system_watcher_unsupported.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698