diff options
author | sdefresne <sdefresne@chromium.org> | 2015-09-18 03:33:13 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-09-18 10:33:52 +0000 |
commit | 8ba0b88ca93a3ec2fc75e5fb3afdc49775313ad3 (patch) | |
tree | d4063a043beb9a4a037121e9163777c32517837b /components/crash/content/tools | |
parent | e14607adb75b409558e84b997bfcc294771ab543 (diff) | |
download | chromium_src-8ba0b88ca93a3ec2fc75e5fb3afdc49775313ad3.zip chromium_src-8ba0b88ca93a3ec2fc75e5fb3afdc49775313ad3.tar.gz chromium_src-8ba0b88ca93a3ec2fc75e5fb3afdc49775313ad3.tar.bz2 |
Turn components/crash into a layered component.
The crash component cannot be used on iOS (as it depends on //content).
Turn it into a layered component so that it is possible to put shared
code related to crash there (components/crash_keys and objc_zombie.{h,mm}).
BUG=522955
TBR=sky@chromium.org
TBR=jschuh@chromium.org
NOPRESUBMIT=true
Review URL: https://codereview.chromium.org/1315303004
Cr-Commit-Position: refs/heads/master@{#349643}
Diffstat (limited to 'components/crash/content/tools')
-rw-r--r-- | components/crash/content/tools/BUILD.gn | 18 | ||||
-rw-r--r-- | components/crash/content/tools/crash_service.cc | 487 | ||||
-rw-r--r-- | components/crash/content/tools/crash_service.h | 125 | ||||
-rwxr-xr-x | components/crash/content/tools/dmp2minidump.py | 51 | ||||
-rwxr-xr-x | components/crash/content/tools/generate_breakpad_symbols.py | 262 |
5 files changed, 943 insertions, 0 deletions
diff --git a/components/crash/content/tools/BUILD.gn b/components/crash/content/tools/BUILD.gn new file mode 100644 index 0000000..82f282b --- /dev/null +++ b/components/crash/content/tools/BUILD.gn @@ -0,0 +1,18 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if (is_win) { + source_set("crash_service") { + sources = [ + "crash_service.cc", + "crash_service.h", + ] + + deps = [ + "//base", + "//breakpad:breakpad_handler", + "//breakpad:breakpad_sender", + ] + } +} diff --git a/components/crash/content/tools/crash_service.cc b/components/crash/content/tools/crash_service.cc new file mode 100644 index 0000000..ed55bbf --- /dev/null +++ b/components/crash/content/tools/crash_service.cc @@ -0,0 +1,487 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/crash/content/tools/crash_service.h" + +#include <windows.h> + +#include <sddl.h> +#include <fstream> +#include <map> + +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/win/windows_version.h" +#include "breakpad/src/client/windows/crash_generation/client_info.h" +#include "breakpad/src/client/windows/crash_generation/crash_generation_server.h" +#include "breakpad/src/client/windows/sender/crash_report_sender.h" + +namespace breakpad { + +namespace { + +const wchar_t kTestPipeName[] = L"\\\\.\\pipe\\ChromeCrashServices"; + +const wchar_t kCrashReportURL[] = L"https://clients2.google.com/cr/report"; +const wchar_t kCheckPointFile[] = L"crash_checkpoint.txt"; + +typedef std::map<std::wstring, std::wstring> CrashMap; + +bool CustomInfoToMap(const google_breakpad::ClientInfo* client_info, + const std::wstring& reporter_tag, CrashMap* map) { + google_breakpad::CustomClientInfo info = client_info->GetCustomInfo(); + + for (uintptr_t i = 0; i < info.count; ++i) { + (*map)[info.entries[i].name] = info.entries[i].value; + } + + (*map)[L"rept"] = reporter_tag; + + return !map->empty(); +} + +bool WriteCustomInfoToFile(const std::wstring& dump_path, const CrashMap& map) { + std::wstring file_path(dump_path); + size_t last_dot = file_path.rfind(L'.'); + if (last_dot == std::wstring::npos) + return false; + file_path.resize(last_dot); + file_path += L".txt"; + + std::wofstream file(file_path.c_str(), + std::ios_base::out | std::ios_base::app | std::ios::binary); + if (!file.is_open()) + return false; + + CrashMap::const_iterator pos; + for (pos = map.begin(); pos != map.end(); ++pos) { + std::wstring line = pos->first; + line += L':'; + line += pos->second; + line += L'\n'; + file.write(line.c_str(), static_cast<std::streamsize>(line.length())); + } + return true; +} + +// The window procedure task is to handle when a) the user logs off. +// b) the system shuts down or c) when the user closes the window. +LRESULT __stdcall CrashSvcWndProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + switch (message) { + case WM_CLOSE: + case WM_ENDSESSION: + case WM_DESTROY: + PostQuitMessage(0); + break; + default: + return DefWindowProc(hwnd, message, wparam, lparam); + } + return 0; +} + +// This is the main and only application window. +HWND g_top_window = NULL; + +bool CreateTopWindow(HINSTANCE instance, bool visible) { + WNDCLASSEXW wcx = {0}; + wcx.cbSize = sizeof(wcx); + wcx.style = CS_HREDRAW | CS_VREDRAW; + wcx.lpfnWndProc = CrashSvcWndProc; + wcx.hInstance = instance; + wcx.lpszClassName = L"crash_svc_class"; + ::RegisterClassExW(&wcx); + DWORD style = visible ? WS_POPUPWINDOW | WS_VISIBLE : WS_OVERLAPPED; + + // The window size is zero but being a popup window still shows in the + // task bar and can be closed using the system menu or using task manager. + HWND window = CreateWindowExW(0, wcx.lpszClassName, L"crash service", style, + CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, + NULL, NULL, instance, NULL); + if (!window) + return false; + + ::UpdateWindow(window); + VLOG(1) << "window handle is " << window; + g_top_window = window; + return true; +} + +// Simple helper class to keep the process alive until the current request +// finishes. +class ProcessingLock { + public: + ProcessingLock() { + ::InterlockedIncrement(&op_count_); + } + ~ProcessingLock() { + ::InterlockedDecrement(&op_count_); + } + static bool IsWorking() { + return (op_count_ != 0); + } + private: + static volatile LONG op_count_; +}; + +volatile LONG ProcessingLock::op_count_ = 0; + +// This structure contains the information that the worker thread needs to +// send a crash dump to the server. +struct DumpJobInfo { + DWORD pid; + CrashService* self; + CrashMap map; + std::wstring dump_path; + + DumpJobInfo(DWORD process_id, CrashService* service, + const CrashMap& crash_map, const std::wstring& path) + : pid(process_id), self(service), map(crash_map), dump_path(path) { + } +}; + +} // namespace + +// Command line switches: +const char CrashService::kMaxReports[] = "max-reports"; +const char CrashService::kNoWindow[] = "no-window"; +const char CrashService::kReporterTag[] = "reporter"; +const char CrashService::kDumpsDir[] = "dumps-dir"; +const char CrashService::kPipeName[] = "pipe-name"; + +CrashService::CrashService() + : dumper_(NULL), + sender_(NULL), + requests_handled_(0), + requests_sent_(0), + clients_connected_(0), + clients_terminated_(0) { +} + +CrashService::~CrashService() { + base::AutoLock lock(sending_); + delete dumper_; + delete sender_; +} + +bool CrashService::Initialize(const base::FilePath& operating_dir, + const base::FilePath& dumps_path) { + using google_breakpad::CrashReportSender; + using google_breakpad::CrashGenerationServer; + + std::wstring pipe_name = kTestPipeName; + int max_reports = -1; + + // The checkpoint file allows CrashReportSender to enforce the the maximum + // reports per day quota. Does not seem to serve any other purpose. + base::FilePath checkpoint_path = operating_dir.Append(kCheckPointFile); + + base::CommandLine& cmd_line = *base::CommandLine::ForCurrentProcess(); + + base::FilePath dumps_path_to_use = dumps_path; + + if (cmd_line.HasSwitch(kDumpsDir)) { + dumps_path_to_use = + base::FilePath(cmd_line.GetSwitchValueNative(kDumpsDir)); + } + + // We can override the send reports quota with a command line switch. + if (cmd_line.HasSwitch(kMaxReports)) + max_reports = _wtoi(cmd_line.GetSwitchValueNative(kMaxReports).c_str()); + + // Allow the global pipe name to be overridden for better testability. + if (cmd_line.HasSwitch(kPipeName)) + pipe_name = cmd_line.GetSwitchValueNative(kPipeName); + +#ifdef _WIN64 + pipe_name += L"-x64"; +#endif + + if (max_reports > 0) { + // Create the http sender object. + sender_ = new CrashReportSender(checkpoint_path.value()); + sender_->set_max_reports_per_day(max_reports); + } + + SECURITY_ATTRIBUTES security_attributes = {0}; + SECURITY_ATTRIBUTES* security_attributes_actual = NULL; + + if (base::win::GetVersion() >= base::win::VERSION_VISTA) { + SECURITY_DESCRIPTOR* security_descriptor = + reinterpret_cast<SECURITY_DESCRIPTOR*>( + GetSecurityDescriptorForLowIntegrity()); + DCHECK(security_descriptor != NULL); + + security_attributes.nLength = sizeof(security_attributes); + security_attributes.lpSecurityDescriptor = security_descriptor; + security_attributes.bInheritHandle = FALSE; + + security_attributes_actual = &security_attributes; + } + + // Create the OOP crash generator object. + dumper_ = new CrashGenerationServer(pipe_name, security_attributes_actual, + &CrashService::OnClientConnected, this, + &CrashService::OnClientDumpRequest, this, + &CrashService::OnClientExited, this, + NULL, NULL, + true, &dumps_path_to_use.value()); + + if (!dumper_) { + LOG(ERROR) << "could not create dumper"; + if (security_attributes.lpSecurityDescriptor) + LocalFree(security_attributes.lpSecurityDescriptor); + return false; + } + + if (!CreateTopWindow(::GetModuleHandleW(NULL), + !cmd_line.HasSwitch(kNoWindow))) { + LOG(ERROR) << "could not create window"; + if (security_attributes.lpSecurityDescriptor) + LocalFree(security_attributes.lpSecurityDescriptor); + return false; + } + + reporter_tag_ = L"crash svc"; + if (cmd_line.HasSwitch(kReporterTag)) + reporter_tag_ = cmd_line.GetSwitchValueNative(kReporterTag); + + // Log basic information. + VLOG(1) << "pipe name is " << pipe_name + << "\ndumps at " << dumps_path_to_use.value(); + + if (sender_) { + VLOG(1) << "checkpoint is " << checkpoint_path.value() + << "\nserver is " << kCrashReportURL + << "\nmaximum " << sender_->max_reports_per_day() << " reports/day" + << "\nreporter is " << reporter_tag_; + } + // Start servicing clients. + if (!dumper_->Start()) { + LOG(ERROR) << "could not start dumper"; + if (security_attributes.lpSecurityDescriptor) + LocalFree(security_attributes.lpSecurityDescriptor); + return false; + } + + if (security_attributes.lpSecurityDescriptor) + LocalFree(security_attributes.lpSecurityDescriptor); + + // This is throwaway code. We don't need to sync with the browser process + // once Google Update is updated to a version supporting OOP crash handling. + // Create or open an event to signal the browser process that the crash + // service is initialized. + HANDLE running_event = + ::CreateEventW(NULL, TRUE, TRUE, L"g_chrome_crash_svc"); + // If the browser already had the event open, the CreateEvent call did not + // signal it. We need to do it manually. + ::SetEvent(running_event); + + return true; +} + +void CrashService::OnClientConnected(void* context, + const google_breakpad::ClientInfo* client_info) { + ProcessingLock lock; + VLOG(1) << "client start. pid = " << client_info->pid(); + CrashService* self = static_cast<CrashService*>(context); + ::InterlockedIncrement(&self->clients_connected_); +} + +void CrashService::OnClientExited(void* context, + const google_breakpad::ClientInfo* client_info) { + ProcessingLock lock; + VLOG(1) << "client end. pid = " << client_info->pid(); + CrashService* self = static_cast<CrashService*>(context); + ::InterlockedIncrement(&self->clients_terminated_); + + if (!self->sender_) + return; + + // When we are instructed to send reports we need to exit if there are + // no more clients to service. The next client that runs will start us. + // Only chrome.exe starts crash_service with a non-zero max_reports. + if (self->clients_connected_ > self->clients_terminated_) + return; + if (self->sender_->max_reports_per_day() > 0) { + // Wait for the other thread to send crashes, if applicable. The sender + // thread takes the sending_ lock, so the sleep is just to give it a + // chance to start. + ::Sleep(1000); + base::AutoLock lock(self->sending_); + // Some people can restart chrome very fast, check again if we have + // a new client before exiting for real. + if (self->clients_connected_ == self->clients_terminated_) { + VLOG(1) << "zero clients. exiting"; + ::PostMessage(g_top_window, WM_CLOSE, 0, 0); + } + } +} + +void CrashService::OnClientDumpRequest(void* context, + const google_breakpad::ClientInfo* client_info, + const std::wstring* file_path) { + ProcessingLock lock; + + if (!file_path) { + LOG(ERROR) << "dump with no file path"; + return; + } + if (!client_info) { + LOG(ERROR) << "dump with no client info"; + return; + } + + CrashService* self = static_cast<CrashService*>(context); + if (!self) { + LOG(ERROR) << "dump with no context"; + return; + } + + CrashMap map; + CustomInfoToMap(client_info, self->reporter_tag_, &map); + + // Move dump file to the directory under client breakpad dump location. + base::FilePath dump_location = base::FilePath(*file_path); + CrashMap::const_iterator it = map.find(L"breakpad-dump-location"); + if (it != map.end()) { + base::FilePath alternate_dump_location = base::FilePath(it->second); + base::CreateDirectoryW(alternate_dump_location); + alternate_dump_location = alternate_dump_location.Append( + dump_location.BaseName()); + base::Move(dump_location, alternate_dump_location); + dump_location = alternate_dump_location; + } + + DWORD pid = client_info->pid(); + VLOG(1) << "dump for pid = " << pid << " is " << dump_location.value(); + + if (!WriteCustomInfoToFile(dump_location.value(), map)) { + LOG(ERROR) << "could not write custom info file"; + } + + if (!self->sender_) + return; + + // Send the crash dump using a worker thread. This operation has retry + // logic in case there is no internet connection at the time. + DumpJobInfo* dump_job = new DumpJobInfo(pid, self, map, + dump_location.value()); + if (!::QueueUserWorkItem(&CrashService::AsyncSendDump, + dump_job, WT_EXECUTELONGFUNCTION)) { + LOG(ERROR) << "could not queue job"; + } +} + +// We are going to try sending the report several times. If we can't send, +// we sleep from one minute to several hours depending on the retry round. +unsigned long CrashService::AsyncSendDump(void* context) { + if (!context) + return 0; + + DumpJobInfo* info = static_cast<DumpJobInfo*>(context); + + std::wstring report_id = L"<unsent>"; + + const DWORD kOneMinute = 60*1000; + const DWORD kOneHour = 60*kOneMinute; + + const DWORD kSleepSchedule[] = { + 24*kOneHour, + 8*kOneHour, + 4*kOneHour, + kOneHour, + 15*kOneMinute, + 0}; + + int retry_round = arraysize(kSleepSchedule) - 1; + + do { + ::Sleep(kSleepSchedule[retry_round]); + { + // Take the server lock while sending. This also prevent early + // termination of the service object. + base::AutoLock lock(info->self->sending_); + VLOG(1) << "trying to send report for pid = " << info->pid; + google_breakpad::ReportResult send_result + = info->self->sender_->SendCrashReport(kCrashReportURL, info->map, + info->dump_path, &report_id); + switch (send_result) { + case google_breakpad::RESULT_FAILED: + report_id = L"<network issue>"; + break; + case google_breakpad::RESULT_REJECTED: + report_id = L"<rejected>"; + ++info->self->requests_handled_; + retry_round = 0; + break; + case google_breakpad::RESULT_SUCCEEDED: + ++info->self->requests_sent_; + ++info->self->requests_handled_; + retry_round = 0; + break; + case google_breakpad::RESULT_THROTTLED: + report_id = L"<throttled>"; + break; + default: + report_id = L"<unknown>"; + break; + }; + } + + VLOG(1) << "dump for pid =" << info->pid << " crash2 id =" << report_id; + --retry_round; + } while (retry_round >= 0); + + if (!::DeleteFileW(info->dump_path.c_str())) + LOG(WARNING) << "could not delete " << info->dump_path; + + delete info; + return 0; +} + +int CrashService::ProcessingLoop() { + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + VLOG(1) << "session ending.."; + while (ProcessingLock::IsWorking()) { + ::Sleep(50); + } + + VLOG(1) << "clients connected :" << clients_connected_ + << "\nclients terminated :" << clients_terminated_ + << "\ndumps serviced :" << requests_handled_ + << "\ndumps reported :" << requests_sent_; + + return static_cast<int>(msg.wParam); +} + +PSECURITY_DESCRIPTOR CrashService::GetSecurityDescriptorForLowIntegrity() { + // Build the SDDL string for the label. + std::wstring sddl = L"S:(ML;;NW;;;S-1-16-4096)"; + + PSECURITY_DESCRIPTOR sec_desc = NULL; + + PACL sacl = NULL; + BOOL sacl_present = FALSE; + BOOL sacl_defaulted = FALSE; + + if (::ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), + SDDL_REVISION, + &sec_desc, NULL)) { + if (::GetSecurityDescriptorSacl(sec_desc, &sacl_present, &sacl, + &sacl_defaulted)) { + return sec_desc; + } + } + + return NULL; +} + +} // namespace breakpad diff --git a/components/crash/content/tools/crash_service.h b/components/crash/content/tools/crash_service.h new file mode 100644 index 0000000..39335e3 --- /dev/null +++ b/components/crash/content/tools/crash_service.h @@ -0,0 +1,125 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRASH_CONTENT_TOOLS_CRASH_SERVICE_H_ +#define COMPONENTS_CRASH_CONTENT_TOOLS_CRASH_SERVICE_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/files/file_path.h" +#include "base/synchronization/lock.h" + +namespace google_breakpad { + +class CrashReportSender; +class CrashGenerationServer; +class ClientInfo; + +} + +namespace breakpad { + +// This class implements an out-of-process crash server. It uses breakpad's +// CrashGenerationServer and CrashReportSender to generate and then send the +// crash dumps. Internally, it uses OS specific pipe to allow applications to +// register for crash dumps and later on when a registered application crashes +// it will signal an event that causes this code to wake up and perform a +// crash dump on the signaling process. The dump is then stored on disk and +// possibly sent to the crash2 servers. +class CrashService { + public: + CrashService(); + ~CrashService(); + + // Starts servicing crash dumps. Returns false if it failed. Do not use + // other members in that case. |operating_dir| is where the CrashService + // should store breakpad's checkpoint file. |dumps_path| is the directory + // where the crash dumps should be stored. + bool Initialize(const base::FilePath& operating_dir, + const base::FilePath& dumps_path); + + // Command line switches: + // + // --max-reports=<number> + // Allows to override the maximum number for reports per day. Normally + // the crash dumps are never sent so if you want to send any you must + // specify a positive number here. + static const char kMaxReports[]; + // --no-window + // Does not create a visible window on the desktop. The window does not have + // any other functionality other than allowing the crash service to be + // gracefully closed. + static const char kNoWindow[]; + // --reporter=<string> + // Allows to specify a custom string that appears on the detail crash report + // page in the crash server. This should be a 25 chars or less string. + // The default tag if not specified is 'crash svc'. + static const char kReporterTag[]; + // --dumps-dir=<directory-path> + // Override the directory to which crash dump files will be written. + static const char kDumpsDir[]; + // --pipe-name=<string> + // Override the name of the Windows named pipe on which we will + // listen for crash dump request messages. + static const char kPipeName[]; + + // Returns number of crash dumps handled. + int requests_handled() const { + return requests_handled_; + } + // Returns number of crash clients registered. + int clients_connected() const { + return clients_connected_; + } + // Returns number of crash clients terminated. + int clients_terminated() const { + return clients_terminated_; + } + + // Starts the processing loop. This function does not return unless the + // user is logging off or the user closes the crash service window. The + // return value is a good number to pass in ExitProcess(). + int ProcessingLoop(); + + private: + static void OnClientConnected(void* context, + const google_breakpad::ClientInfo* client_info); + + static void OnClientDumpRequest( + void* context, + const google_breakpad::ClientInfo* client_info, + const std::wstring* file_path); + + static void OnClientExited(void* context, + const google_breakpad::ClientInfo* client_info); + + // This routine sends the crash dump to the server. It takes the sending_ + // lock when it is performing the send. + static unsigned long __stdcall AsyncSendDump(void* context); + + // Returns the security descriptor which access to low integrity processes + // The caller is supposed to free the security descriptor by calling + // LocalFree. + PSECURITY_DESCRIPTOR GetSecurityDescriptorForLowIntegrity(); + + google_breakpad::CrashGenerationServer* dumper_; + google_breakpad::CrashReportSender* sender_; + + // the extra tag sent to the server with each dump. + std::wstring reporter_tag_; + + // clients serviced statistics: + int requests_handled_; + int requests_sent_; + volatile long clients_connected_; + volatile long clients_terminated_; + base::Lock sending_; + + DISALLOW_COPY_AND_ASSIGN(CrashService); +}; + +} // namespace breakpad + +#endif // COMPONENTS_CRASH_CONTENT_TOOLS_CRASH_SERVICE_H_ diff --git a/components/crash/content/tools/dmp2minidump.py b/components/crash/content/tools/dmp2minidump.py new file mode 100755 index 0000000..7823d48 --- /dev/null +++ b/components/crash/content/tools/dmp2minidump.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""A tool to extract minidumps from dmp crash dumps.""" + +import os +import sys +from cgi import parse_multipart + + +def ProcessDump(dump_file, minidump_file): + """Extracts the part of the dump file that minidump_stackwalk can read. + + The dump files generated by the breakpad integration multi-part form data + that include the minidump as file attachment. + + Args: + dump_file: the dump file that needs to be processed. + minidump_file: the file to write the minidump to. + """ + try: + dump = open(dump_file, 'rb') + boundary = dump.readline().strip()[2:] + data = parse_multipart(dump, {'boundary': boundary}) + except: + print 'Failed to read dmp file %s' % dump_file + return + + if not 'upload_file_minidump' in data: + print 'Could not find minidump file in dump.' + return + + f = open(minidump_file, 'w') + f.write("\r\n".join(data['upload_file_minidump'])) + f.close() + + +def main(): + if len(sys.argv) != 3: + print 'Usage: %s [dmp file] [minidump]' % sys.argv[0] + print '' + print 'Extracts the minidump stored in the crash dump file' + return 1 + + ProcessDump(sys.argv[1], sys.argv[2]) + + +if '__main__' == __name__: + sys.exit(main()) diff --git a/components/crash/content/tools/generate_breakpad_symbols.py b/components/crash/content/tools/generate_breakpad_symbols.py new file mode 100755 index 0000000..5f8fb85 --- /dev/null +++ b/components/crash/content/tools/generate_breakpad_symbols.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""A tool to generate symbols for a binary suitable for breakpad. + +Currently, the tool only supports Linux, Android, and Mac. Support for other +platforms is planned. +""" + +import errno +import optparse +import os +import Queue +import re +import shutil +import subprocess +import sys +import threading + + +CONCURRENT_TASKS=4 + + +def GetCommandOutput(command): + """Runs the command list, returning its output. + + Prints the given command (which should be a list of one or more strings), + then runs it and returns its output (stdout) as a string. + + From chromium_utils. + """ + devnull = open(os.devnull, 'w') + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=devnull, + bufsize=1) + output = proc.communicate()[0] + return output + + +def GetDumpSymsBinary(build_dir=None): + """Returns the path to the dump_syms binary.""" + DUMP_SYMS = 'dump_syms' + dump_syms_bin = os.path.join(os.path.expanduser(build_dir), DUMP_SYMS) + if not os.access(dump_syms_bin, os.X_OK): + print 'Cannot find %s.' % dump_syms_bin + return None + + return dump_syms_bin + + +def Resolve(path, exe_path, loader_path, rpaths): + """Resolve a dyld path. + + @executable_path is replaced with |exe_path| + @loader_path is replaced with |loader_path| + @rpath is replaced with the first path in |rpaths| where the referenced file + is found + """ + path = path.replace('@loader_path', loader_path) + path = path.replace('@executable_path', exe_path) + if path.find('@rpath') != -1: + for rpath in rpaths: + new_path = Resolve(path.replace('@rpath', rpath), exe_path, loader_path, + []) + if os.access(new_path, os.X_OK): + return new_path + return '' + return path + + +def GetSharedLibraryDependenciesLinux(binary): + """Return absolute paths to all shared library dependecies of the binary. + + This implementation assumes that we're running on a Linux system.""" + ldd = GetCommandOutput(['ldd', binary]) + lib_re = re.compile('\t.* => (.+) \(.*\)$') + result = [] + for line in ldd.splitlines(): + m = lib_re.match(line) + if m: + result.append(m.group(1)) + return result + + +def GetSharedLibraryDependenciesMac(binary, exe_path): + """Return absolute paths to all shared library dependecies of the binary. + + This implementation assumes that we're running on a Mac system.""" + loader_path = os.path.dirname(binary) + otool = GetCommandOutput(['otool', '-l', binary]).splitlines() + rpaths = [] + for idx, line in enumerate(otool): + if line.find('cmd LC_RPATH') != -1: + m = re.match(' *path (.*) \(offset .*\)$', otool[idx+2]) + rpaths.append(m.group(1)) + + otool = GetCommandOutput(['otool', '-L', binary]).splitlines() + lib_re = re.compile('\t(.*) \(compatibility .*\)$') + deps = [] + for line in otool: + m = lib_re.match(line) + if m: + dep = Resolve(m.group(1), exe_path, loader_path, rpaths) + if dep: + deps.append(os.path.normpath(dep)) + return deps + + +def GetSharedLibraryDependencies(options, binary, exe_path): + """Return absolute paths to all shared library dependecies of the binary.""" + deps = [] + if sys.platform.startswith('linux'): + deps = GetSharedLibraryDependenciesLinux(binary) + elif sys.platform == 'darwin': + deps = GetSharedLibraryDependenciesMac(binary, exe_path) + else: + print "Platform not supported." + sys.exit(1) + + result = [] + build_dir = os.path.abspath(options.build_dir) + for dep in deps: + if (os.access(dep, os.X_OK) and + os.path.abspath(os.path.dirname(dep)).startswith(build_dir)): + result.append(dep) + return result + + +def mkdir_p(path): + """Simulates mkdir -p.""" + try: + os.makedirs(path) + except OSError as e: + if e.errno == errno.EEXIST and os.path.isdir(path): + pass + else: raise + + +def GenerateSymbols(options, binaries): + """Dumps the symbols of binary and places them in the given directory.""" + + queue = Queue.Queue() + print_lock = threading.Lock() + + def _Worker(): + while True: + binary = queue.get() + + should_dump_syms = True + reason = "no reason" + + output_path = os.path.join( + options.symbols_dir, os.path.basename(binary)) + if os.path.isdir(output_path): + if os.path.getmtime(binary) < os.path.getmtime(output_path): + should_dump_syms = False + reason = "symbols are more current than binary" + + if not should_dump_syms: + if options.verbose: + with print_lock: + print "Skipping %s (%s)" % (binary, reason) + queue.task_done() + continue + + if options.verbose: + with print_lock: + print "Generating symbols for %s" % binary + + if os.path.isdir(output_path): + os.utime(output_path, None) + + syms = GetCommandOutput([GetDumpSymsBinary(options.build_dir), '-r', + binary]) + module_line = re.match("MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n", syms) + output_path = os.path.join(options.symbols_dir, module_line.group(2), + module_line.group(1)) + mkdir_p(output_path) + symbol_file = "%s.sym" % module_line.group(2) + try: + f = open(os.path.join(output_path, symbol_file), 'w') + f.write(syms) + f.close() + except Exception, e: + # Not much we can do about this. + with print_lock: + print e + + queue.task_done() + + for binary in binaries: + queue.put(binary) + + for _ in range(options.jobs): + t = threading.Thread(target=_Worker) + t.daemon = True + t.start() + + queue.join() + + +def main(): + parser = optparse.OptionParser() + parser.add_option('', '--build-dir', default='', + help='The build output directory.') + parser.add_option('', '--symbols-dir', default='', + help='The directory where to write the symbols file.') + parser.add_option('', '--binary', default='', + help='The path of the binary to generate symbols for.') + parser.add_option('', '--clear', default=False, action='store_true', + help='Clear the symbols directory before writing new ' + 'symbols.') + parser.add_option('-j', '--jobs', default=CONCURRENT_TASKS, action='store', + type='int', help='Number of parallel tasks to run.') + parser.add_option('-v', '--verbose', action='store_true', + help='Print verbose status output.') + + (options, _) = parser.parse_args() + + if not options.symbols_dir: + print "Required option --symbols-dir missing." + return 1 + + if not options.build_dir: + print "Required option --build-dir missing." + return 1 + + if not options.binary: + print "Required option --binary missing." + return 1 + + if not os.access(options.binary, os.X_OK): + print "Cannot find %s." % options.binary + return 1 + + if options.clear: + try: + shutil.rmtree(options.symbols_dir) + except: + pass + + if not GetDumpSymsBinary(options.build_dir): + return 1 + + # Build the transitive closure of all dependencies. + binaries = set([options.binary]) + queue = [options.binary] + exe_path = os.path.dirname(options.binary) + while queue: + deps = GetSharedLibraryDependencies(options, queue.pop(0), exe_path) + new_deps = set(deps) - binaries + binaries |= new_deps + queue.extend(list(new_deps)) + + GenerateSymbols(options, binaries) + + return 0 + + +if '__main__' == __name__: + sys.exit(main()) |