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

Side by Side Diff: tools/goopdump/data_dumper_goopdate.cc

Issue 624713003: Keep only base/extractor.[cc|h]. (Closed) Base URL: https://chromium.googlesource.com/external/omaha.git@master
Patch Set: Created 6 years, 2 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
« no previous file with comments | « tools/goopdump/data_dumper_goopdate.h ('k') | tools/goopdump/data_dumper_network.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2008-2009 Google Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 // ========================================================================
15
16
17 #include "omaha/tools/goopdump/data_dumper_goopdate.h"
18
19 #include <atltime.h>
20 #include <mstask.h>
21 #include <psapi.h>
22 #include <regstr.h>
23 #include <tlhelp32.h>
24
25 #include <list>
26
27 #include "omaha/common/constants.h"
28 #include "omaha/common/error.h"
29 #include "omaha/common/file.h"
30 #include "omaha/common/file_reader.h"
31 #include "omaha/common/reg_key.h"
32 #include "omaha/common/scoped_ptr_cotask.h"
33 #include "omaha/common/service_utils.h"
34 #include "omaha/common/time.h"
35 #include "omaha/goopdate/config_manager.h"
36 #include "omaha/goopdate/const_goopdate.h"
37 #include "omaha/goopdate/event_logger.h"
38 #include "omaha/goopdate/goopdate_utils.h"
39 #include "omaha/tools/goopdump/dump_log.h"
40 #include "omaha/tools/goopdump/goopdump_cmd_line_parser.h"
41 #include "omaha/tools/goopdump/process_commandline.h"
42
43 namespace {
44
45 CString FormatRunTimeString(SYSTEMTIME* system_time) {
46 SYSTEMTIME local_time = {0};
47 ::SystemTimeToTzSpecificLocalTime(NULL, system_time, &local_time);
48 CString str;
49 str.Format(_T("%02d/%02d/%04d %02d:%02d:%02d"),
50 local_time.wMonth,
51 local_time.wDay,
52 local_time.wYear,
53 local_time.wHour,
54 local_time.wMinute,
55 local_time.wSecond);
56 return str;
57 }
58
59 } // namespace
60
61 namespace omaha {
62
63 DataDumperGoopdate::DataDumperGoopdate() {
64 }
65
66 DataDumperGoopdate::~DataDumperGoopdate() {
67 }
68
69 HRESULT DataDumperGoopdate::Process(const DumpLog& dump_log,
70 const GoopdumpCmdLineArgs& args) {
71 UNREFERENCED_PARAMETER(args);
72
73 DumpHeader header(dump_log, _T("Goopdate Data"));
74
75 dump_log.WriteLine(_T(""));
76 dump_log.WriteLine(_T("-- GENERAL / GLOBAL DATA --"));
77 DumpHostsFile(dump_log);
78 DumpGoogleUpdateIniFile(dump_log);
79 DumpUpdateDevKeys(dump_log);
80 DumpLogFile(dump_log);
81 DumpEventLog(dump_log);
82 DumpGoogleUpdateProcessInfo(dump_log);
83
84 if (args.is_machine) {
85 dump_log.WriteLine(_T(""));
86 dump_log.WriteLine(_T("-- PER-MACHINE DATA --"));
87 DumpDirContents(dump_log, true);
88 DumpServiceInfo(dump_log);
89 }
90
91 if (args.is_user) {
92 dump_log.WriteLine(_T(""));
93 dump_log.WriteLine(_T("-- PER-USER DATA --"));
94 DumpDirContents(dump_log, false);
95 DumpRunKeys(dump_log);
96 }
97
98 DumpScheduledTaskInfo(dump_log, args.is_machine);
99
100 return S_OK;
101 }
102
103 void DataDumperGoopdate::DumpDirContents(const DumpLog& dump_log,
104 bool is_machine) {
105 DumpHeader header(dump_log, _T("Directory Contents"));
106
107 CString registered_version;
108 if (FAILED(GetRegisteredVersion(is_machine, &registered_version))) {
109 dump_log.WriteLine(_T("Failed to get registered version."));
110 return;
111 }
112
113 CString dll_dir;
114 if (FAILED(GetDllDir(is_machine, &dll_dir))) {
115 dump_log.WriteLine(_T("Failed to get dlldir."));
116 return;
117 }
118
119 dump_log.WriteLine(_T("Version:\t%s"), registered_version);
120 dump_log.WriteLine(_T("Dll Dir:\t%s"), dll_dir);
121
122 // Enumerate all files in the DllPath and log them.
123 std::vector<CString> matching_paths;
124 HRESULT hr = File::GetWildcards(dll_dir, _T("*.*"), &matching_paths);
125 if (SUCCEEDED(hr)) {
126 dump_log.WriteLine(_T(""));
127 dump_log.WriteLine(_T("Files in DllDir:"));
128 for (size_t i = 0; i < matching_paths.size(); ++i) {
129 dump_log.WriteLine(matching_paths[i]);
130 }
131 dump_log.WriteLine(_T(""));
132 } else {
133 dump_log.WriteLine(_T("Failure getting files in DllDir (0x%x)."), hr);
134 }
135 }
136
137 HRESULT DataDumperGoopdate::GetRegisteredVersion(bool is_machine,
138 CString* version) {
139 HKEY key = NULL;
140 LONG res = ::RegOpenKeyEx(is_machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER,
141 GOOPDATE_REG_RELATIVE_CLIENTS GOOPDATE_APP_ID,
142 0,
143 KEY_READ,
144 &key);
145 if (ERROR_SUCCESS != res) {
146 return HRESULT_FROM_WIN32(res);
147 }
148
149 DWORD type = 0;
150 DWORD version_length = 50;
151 res = ::SHQueryValueEx(key,
152 omaha::kRegValueProductVersion,
153 NULL,
154 &type,
155 CStrBuf(*version, version_length),
156 &version_length);
157 if (ERROR_SUCCESS != res) {
158 return HRESULT_FROM_WIN32(res);
159 }
160
161 if (REG_SZ != type) {
162 return E_UNEXPECTED;
163 }
164
165 return S_OK;
166 }
167
168 HRESULT DataDumperGoopdate::GetDllDir(bool is_machine, CString* dll_path) {
169 TCHAR path[MAX_PATH] = {0};
170
171 CString base_path = goopdate_utils::BuildGoogleUpdateExeDir(is_machine);
172
173 // Try the side-by-side DLL first.
174 _tcscpy_s(path, arraysize(path), base_path);
175 if (!::PathAppend(path, omaha::kGoopdateDllName)) {
176 return HRESULTFromLastError();
177 }
178 if (File::Exists(path)) {
179 *dll_path = base_path;
180 return S_OK;
181 }
182
183 // Try the version subdirectory.
184 _tcscpy_s(path, arraysize(path), base_path);
185 CString version;
186 HRESULT hr = GetRegisteredVersion(is_machine, &version);
187 if (FAILED(hr)) {
188 return hr;
189 }
190 if (!::PathAppend(path, version)) {
191 return HRESULTFromLastError();
192 }
193 base_path = path;
194 if (!::PathAppend(path, omaha::kGoopdateDllName)) {
195 return HRESULTFromLastError();
196 }
197 if (!File::Exists(path)) {
198 return GOOGLEUPDATE_E_DLL_NOT_FOUND;
199 }
200
201 *dll_path = base_path;
202 return S_OK;
203 }
204
205 void DataDumperGoopdate::DumpGoogleUpdateIniFile(const DumpLog& dump_log) {
206 DumpHeader header(dump_log, _T("GoogleUpdate.ini File Contents"));
207 DumpFileContents(dump_log, _T("c:\\googleupdate.ini"), 0);
208 }
209
210 void DataDumperGoopdate::DumpHostsFile(const DumpLog& dump_log) {
211 DumpHeader header(dump_log, _T("Hosts File Contents"));
212 TCHAR system_path[MAX_PATH] = {0};
213 HRESULT hr = ::SHGetFolderPath(NULL,
214 CSIDL_SYSTEM,
215 NULL,
216 SHGFP_TYPE_CURRENT,
217 system_path);
218 if (FAILED(hr)) {
219 dump_log.WriteLine(_T("Can't get System folder: 0x%x"), hr);
220 return;
221 }
222
223 CPath full_path = system_path;
224 full_path.Append(_T("drivers\\etc\\hosts"));
225 DumpFileContents(dump_log, full_path, 0);
226 }
227
228 void DataDumperGoopdate::DumpUpdateDevKeys(const DumpLog& dump_log) {
229 DumpHeader header(dump_log, _T("UpdateDev Keys"));
230
231 DumpRegistryKeyData(dump_log, _T("HKLM\\Software\\Google\\UpdateDev"));
232 }
233
234 void DataDumperGoopdate::DumpLogFile(const DumpLog& dump_log) {
235 DumpHeader header(dump_log, _T("Debug Log File Contents"));
236
237 Logging logger;
238 CString log_file_path(logger.GetLogFilePath());
239 DumpFileContents(dump_log, log_file_path, 500);
240 }
241
242 CString EventLogTypeToString(WORD event_log_type) {
243 CString str = _T("Unknown");
244 switch (event_log_type) {
245 case EVENTLOG_ERROR_TYPE:
246 str = _T("ERROR");
247 break;
248 case EVENTLOG_WARNING_TYPE:
249 str = _T("WARNING");
250 break;
251 case EVENTLOG_INFORMATION_TYPE:
252 str = _T("INFORMATION");
253 break;
254 case EVENTLOG_AUDIT_SUCCESS:
255 str = _T("AUDIT_SUCCESS");
256 break;
257 case EVENTLOG_AUDIT_FAILURE:
258 str = _T("AUDIT_FAILURE");
259 break;
260 default:
261 str = _T("Unknown");
262 break;
263 }
264
265 return str;
266 }
267
268 void DataDumperGoopdate::DumpEventLog(const DumpLog& dump_log) {
269 DumpHeader header(dump_log, _T("Google Update Event Log Entries"));
270
271 HANDLE handle_event_log = ::OpenEventLog(NULL, _T("Application"));
272 if (handle_event_log == NULL) {
273 return;
274 }
275
276 const int kInitialBufferSize = 8192;
277 int buffer_size = kInitialBufferSize;
278 scoped_array<TCHAR> buffer(new TCHAR[buffer_size]);
279
280 while (1) {
281 EVENTLOGRECORD* record = reinterpret_cast<EVENTLOGRECORD*>(buffer.get());
282 DWORD num_bytes_read = 0;
283 DWORD bytes_needed = 0;
284 if (!::ReadEventLog(handle_event_log,
285 EVENTLOG_FORWARDS_READ | EVENTLOG_SEQUENTIAL_READ,
286 0,
287 record,
288 buffer_size,
289 &num_bytes_read,
290 &bytes_needed)) {
291 const int err = ::GetLastError();
292 if (ERROR_INSUFFICIENT_BUFFER == err) {
293 buffer_size = bytes_needed;
294 buffer.reset(new TCHAR[buffer_size]);
295 continue;
296 } else {
297 if (ERROR_HANDLE_EOF != err) {
298 dump_log.WriteLine(_T("ReadEventLog failed: %d"), err);
299 }
300 break;
301 }
302 }
303
304 while (num_bytes_read > 0) {
305 const TCHAR* source_name = reinterpret_cast<const TCHAR*>(
306 reinterpret_cast<BYTE*>(record) + sizeof(*record));
307
308 if (_tcscmp(source_name, EventLogger::kSourceName) == 0) {
309 CString event_log_type = EventLogTypeToString(record->EventType);
310
311 const TCHAR* message_data_buffer = reinterpret_cast<const TCHAR*>(
312 reinterpret_cast<BYTE*>(record) + record->StringOffset);
313
314 CString message_data(message_data_buffer);
315
316 FILETIME filetime = {0};
317 TimeTToFileTime(record->TimeWritten, &filetime);
318 SYSTEMTIME systemtime = {0};
319 ::FileTimeToSystemTime(&filetime, &systemtime);
320
321 CString message_line;
322 message_line.Format(_T("[%s] (%d)|(%s) %s"),
323 FormatRunTimeString(&systemtime),
324 record->EventID,
325 event_log_type,
326 message_data);
327 dump_log.WriteLine(message_line);
328 }
329
330 num_bytes_read -= record->Length;
331 record = reinterpret_cast<EVENTLOGRECORD*>(
332 reinterpret_cast<BYTE*>(record) + record->Length);
333 }
334
335 record = reinterpret_cast<EVENTLOGRECORD*>(&buffer);
336 }
337
338 ::CloseEventLog(handle_event_log);
339 }
340
341 void DataDumperGoopdate::DumpGoogleUpdateProcessInfo(const DumpLog& dump_log) {
342 DumpHeader header(dump_log, _T("GoogleUpdate.exe Process Info"));
343
344 EnableDebugPrivilege();
345
346 scoped_handle handle_snap;
347 reset(handle_snap,
348 ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0));
349 if (!valid(handle_snap)) {
350 return;
351 }
352
353 PROCESSENTRY32 process_entry32 = {0};
354 process_entry32.dwSize = sizeof(PROCESSENTRY32);
355
356 if (!::Process32First(get(handle_snap), &process_entry32)) {
357 return;
358 }
359
360 bool first = true;
361
362 do {
363 CString exe_file_name = process_entry32.szExeFile;
364 exe_file_name.MakeLower();
365
366 if (exe_file_name.Find(_T("googleupdate.exe")) >= 0) {
367 if (first) {
368 first = false;
369 } else {
370 dump_log.WriteLine(_T("-------------------"));
371 }
372 dump_log.WriteLine(_T("Process ID: %d"), process_entry32.th32ProcessID);
373 scoped_handle process_handle;
374 reset(process_handle, ::OpenProcess(PROCESS_ALL_ACCESS,
375 FALSE,
376 process_entry32.th32ProcessID));
377 if (get(process_handle)) {
378 CString command_line;
379 if (SUCCEEDED(GetProcessCommandLine(process_entry32.th32ProcessID,
380 &command_line))) {
381 dump_log.WriteLine(_T("Command Line: %s"), command_line);
382 } else {
383 dump_log.WriteLine(_T("Command Line: Failed to retrieve"));
384 }
385
386 PROCESS_MEMORY_COUNTERS memory_counters = {0};
387 memory_counters.cb = sizeof(memory_counters);
388 if (GetProcessMemoryInfo(get(process_handle),
389 &memory_counters,
390 sizeof(memory_counters))) {
391 dump_log.WriteLine(_T("Page Fault Count: %d"),
392 memory_counters.PageFaultCount);
393 dump_log.WriteLine(_T("Peak Working Set Size: %d"),
394 memory_counters.PeakWorkingSetSize);
395 dump_log.WriteLine(_T("Working Set Size: %d"),
396 memory_counters.WorkingSetSize);
397 dump_log.WriteLine(_T("Page File Usage: %d"),
398 memory_counters.PagefileUsage);
399 dump_log.WriteLine(_T("Peak Page File Usage: %d"),
400 memory_counters.PeakPagefileUsage);
401 } else {
402 dump_log.WriteLine(_T("Unable to get process memory info"));
403 }
404
405 THREADENTRY32 thread_entry = {0};
406 thread_entry.dwSize = sizeof(thread_entry);
407 int thread_count = 0;
408 if (Thread32First(get(handle_snap), &thread_entry)) {
409 do {
410 if (thread_entry.th32OwnerProcessID ==
411 process_entry32.th32ProcessID) {
412 ++thread_count;
413 }
414 } while (::Thread32Next(get(handle_snap), &thread_entry));
415 }
416
417 dump_log.WriteLine(_T("Thread Count: %d"), thread_count);
418
419 FILETIME creation_time = {0};
420 FILETIME exit_time = {0};
421 FILETIME kernel_time = {0};
422 FILETIME user_time = {0};
423 if (::GetProcessTimes(get(process_handle),
424 &creation_time,
425 &exit_time,
426 &kernel_time,
427 &user_time)) {
428 SYSTEMTIME creation_system_time = {0};
429 FileTimeToSystemTime(&creation_time, &creation_system_time);
430 CString creation_str = FormatRunTimeString(&creation_system_time);
431 dump_log.WriteLine(_T("Process Start Time: %s"), creation_str);
432
433 CTime time_creation(creation_time);
434 CTime time_now = CTime::GetCurrentTime();
435 CTimeSpan time_span = time_now - time_creation;
436 CString time_span_format =
437 time_span.Format(_T("%D days, %H hours, %M minutes, %S seconds"));
438 dump_log.WriteLine(_T("Process Uptime: %s"), time_span_format);
439 } else {
440 dump_log.WriteLine(_T("Unable to get Process Times"));
441 }
442 }
443 }
444 } while (::Process32Next(get(handle_snap), &process_entry32));
445 }
446
447 void DataDumperGoopdate::DumpServiceInfo(const DumpLog& dump_log) {
448 DumpHeader header(dump_log, _T("Google Update Service Info"));
449
450 CString current_service_name = ConfigManager::GetCurrentServiceName();
451 bool is_service_installed =
452 ServiceInstall::IsServiceInstalled(current_service_name);
453
454 dump_log.WriteLine(_T("Service Name: %s"), current_service_name);
455 dump_log.WriteLine(_T("Is Installed: %s"),
456 is_service_installed ? _T("YES") : _T("NO"));
457 }
458
459 void DataDumperGoopdate::DumpScheduledTaskInfo(const DumpLog& dump_log,
460 bool is_machine) {
461 DumpHeader header(dump_log, _T("Scheduled Task Info"));
462
463 CComPtr<ITaskScheduler> scheduler;
464 HRESULT hr = scheduler.CoCreateInstance(CLSID_CTaskScheduler,
465 NULL,
466 CLSCTX_INPROC_SERVER);
467 if (FAILED(hr)) {
468 dump_log.WriteLine(_T("ITaskScheduler.CoCreateInstance failed: 0x%x"),
469 hr);
470 return;
471 }
472
473 CComPtr<ITask> task;
474 hr = scheduler->Activate(ConfigManager::GetCurrentTaskNameCore(is_machine),
475 __uuidof(ITask),
476 reinterpret_cast<IUnknown**>(&task));
477
478 if (FAILED(hr)) {
479 dump_log.WriteLine(_T("ITaskScheduler.Activate failed: 0x%x"), hr);
480 return;
481 }
482
483 scoped_ptr_cotask<TCHAR> app_name;
484 hr = task->GetApplicationName(address(app_name));
485 dump_log.WriteLine(_T("ApplicationName: %s"),
486 SUCCEEDED(hr) ? app_name.get() : _T("Not Found"));
487
488 scoped_ptr_cotask<TCHAR> creator;
489 hr = task->GetCreator(address(creator));
490 dump_log.WriteLine(_T("Creator: %s"),
491 SUCCEEDED(hr) ? creator.get() : _T("Not Found"));
492
493 scoped_ptr_cotask<TCHAR> parameters;
494 hr = task->GetParameters(address(parameters));
495 dump_log.WriteLine(_T("Parameters: %s"),
496 SUCCEEDED(hr) ? parameters.get() : _T("Not Found"));
497
498 scoped_ptr_cotask<TCHAR> comment;
499 hr = task->GetComment(address(comment));
500 dump_log.WriteLine(_T("Comment: %s"),
501 SUCCEEDED(hr) ? comment.get() : _T("Not Found"));
502
503 scoped_ptr_cotask<TCHAR> working_dir;
504 hr = task->GetWorkingDirectory(address(working_dir));
505 dump_log.WriteLine(_T("Working Directory: %s"),
506 SUCCEEDED(hr) ? working_dir.get() : _T("Not Found"));
507
508 scoped_ptr_cotask<TCHAR> account_info;
509 hr = task->GetAccountInformation(address(account_info));
510 dump_log.WriteLine(_T("Account Info: %s"),
511 SUCCEEDED(hr) ? account_info.get() : _T("Not Found"));
512
513 dump_log.WriteLine(_T("Triggers:"));
514 WORD trigger_count = 0;
515 hr = task->GetTriggerCount(&trigger_count);
516 if (SUCCEEDED(hr)) {
517 for (WORD i = 0; i < trigger_count; ++i) {
518 CComPtr<ITaskTrigger> trigger;
519 if (SUCCEEDED(task->GetTrigger(i, &trigger))) {
520 scoped_ptr_cotask<TCHAR> trigger_string;
521 if (SUCCEEDED(trigger->GetTriggerString(address(trigger_string)))) {
522 dump_log.WriteLine(_T(" %s"), trigger_string.get());
523 }
524 }
525 }
526 }
527
528 SYSTEMTIME next_run_time = {0};
529 hr = task->GetNextRunTime(&next_run_time);
530 if (SUCCEEDED(hr)) {
531 dump_log.WriteLine(_T("Next Run Time: %s"),
532 FormatRunTimeString(&next_run_time));
533 } else {
534 dump_log.WriteLine(_T("Next Run Time: Not Found"));
535 }
536
537 SYSTEMTIME recent_run_time = {0};
538 hr = task->GetMostRecentRunTime(&recent_run_time);
539 if (SUCCEEDED(hr)) {
540 dump_log.WriteLine(_T("Most Recent Run Time: %s"),
541 FormatRunTimeString(&recent_run_time));
542 } else {
543 dump_log.WriteLine(_T("Most Recent Run Time: Not Found"));
544 }
545
546 DWORD max_run_time = 0;
547 hr = task->GetMaxRunTime(&max_run_time);
548 if (SUCCEEDED(hr)) {
549 dump_log.WriteLine(_T("Max Run Time: %d ms"), max_run_time);
550 } else {
551 dump_log.WriteLine(_T("Max Run Time: [Not Available]"));
552 }
553 }
554
555 void DataDumperGoopdate::DumpRunKeys(const DumpLog& dump_log) {
556 DumpHeader header(dump_log, _T("Google Update Run Keys"));
557
558 CString key_path = AppendRegKeyPath(USER_KEY_NAME, REGSTR_PATH_RUN);
559 DumpRegValueStr(dump_log, key_path, kRunValueName);
560 }
561
562 void DataDumperGoopdate::DumpFileContents(const DumpLog& dump_log,
563 const CString& file_path,
564 int num_tail_lines) {
565 if (num_tail_lines > 0) {
566 dump_log.WriteLine(_T("Tailing last %d lines of file"), num_tail_lines);
567 }
568 dump_log.WriteLine(_T("-------------------------------------"));
569 if (File::Exists(file_path)) {
570 FileReader reader;
571 HRESULT hr = reader.Init(file_path, 2048);
572 if (FAILED(hr)) {
573 dump_log.WriteLine(_T("Unable to open %s: 0x%x."), file_path, hr);
574 return;
575 }
576
577 CString current_line;
578 std::list<CString> tail_lines;
579
580 while (SUCCEEDED(reader.ReadLineString(&current_line))) {
581 if (num_tail_lines == 0) {
582 // We're not doing a tail, so just print the entire file.
583 dump_log.WriteLine(current_line);
584 } else {
585 // Collect the lines in a queue until we're done.
586 tail_lines.push_back(current_line);
587 if (tail_lines.size() > static_cast<size_t>(num_tail_lines)) {
588 tail_lines.pop_front();
589 }
590 }
591 }
592
593 // Print out the tail lines collected from the file, if they exist.
594 if (num_tail_lines > 0) {
595 for (std::list<CString>::const_iterator it = tail_lines.begin();
596 it != tail_lines.end();
597 ++it) {
598 dump_log.WriteLine(*it);
599 }
600 }
601 } else {
602 dump_log.WriteLine(_T("File does not exist."));
603 }
604 }
605
606 } // namespace omaha
607
OLDNEW
« no previous file with comments | « tools/goopdump/data_dumper_goopdate.h ('k') | tools/goopdump/data_dumper_network.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698