blob: 7acc814ca2cf432a221c0f16284b1874e6db7277 [file] [log] [blame]
// Copyright 2023 The Fuchsia 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 "src/developer/debug/zxdb/client/download_manager.h"
#include <lib/syslog/cpp/macros.h>
#include <map>
#include <memory>
#include "src/developer/debug/shared/logging/logging.h"
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/zxdb/client/download_observer.h"
#include "src/developer/debug/zxdb/client/process.h"
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/symbol_server.h"
#include "src/developer/debug/zxdb/client/system.h"
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/symbols/debug_symbol_file_type.h"
#include "src/developer/debug/zxdb/symbols/loaded_module_symbols.h"
#include "src/developer/debug/zxdb/symbols/process_symbols.h"
#include "src/lib/fxl/strings/string_printf.h"
namespace zxdb {
namespace {
std::string DebugSymbolFileTypeToString(DebugSymbolFileType file_type) {
switch (file_type) {
case DebugSymbolFileType::kDebugInfo:
return "debuginfo";
case DebugSymbolFileType::kBinary:
return "binary";
}
}
} // namespace
// When we want to download symbols for a build ID, we create a Download object. This object is
// owned by the DownloadManager. We then fire off requests to all symbol servers we know about
// asking to download the symbols. These requests are completed in sequence to all of the servers
// the DownloadManager knows about, until one reports that the download succeeded. If all the
// servers are tried and none of them are informed the request was successful, DownloadManager will
// drop the object. The destructor of the Download object calls a callback that handles notifying
// the rest of the system of those results.
//
// If we receive more notifications that other servers are ready to receive download requests, they
// are queued and will be tried as a fallback until the download succeeds or we run out of servers.
class Download {
public:
Download(const std::string& build_id, DebugSymbolFileType file_type,
SymbolServer::FetchCallback result_cb)
: build_id_(build_id),
file_type_(file_type),
result_cb_(std::move(result_cb)),
weak_factory_(this) {
DEBUG_LOG(DownloadManager) << "Starting download for " << build_id_;
}
~Download() { Finish(); }
bool active() const { return !!result_cb_ && download_in_progress_; }
// Add a symbol server to this download.
void AddServer(SymbolServer* server);
private:
// Notify this download object that we have gotten the symbols if we're going to get them.
void Finish();
// Notify this Download object that a transaction failed.
void Error(const Err& err);
size_t failed_server_count_ = 0;
std::string build_id_;
DebugSymbolFileType file_type_;
// Collection for errors encountered during download.
std::map<ErrType, std::vector<std::string>> errors_;
std::string path_;
SymbolServer::FetchCallback result_cb_;
std::vector<fit::callback<void()>> server_fetches_;
bool download_in_progress_ = false;
fxl::WeakPtrFactory<Download> weak_factory_;
};
void Download::Finish() {
if (!result_cb_) {
return;
}
Err final_err;
if (errors_.size() == 1) {
final_err = Err(errors_.begin()->first);
} else if (!errors_.empty()) {
final_err =
Err(fxl::StringPrintf("Multiple errors encountered while downloading %s for build_id %s",
DebugSymbolFileTypeToString(file_type_).c_str(), build_id_.c_str()));
}
// Specialize the error message if the build id was not found on any servers.
if (final_err.type() == ErrType::kNotFound) {
final_err = Err("%s for build_id %s not found on %zu servers\n",
DebugSymbolFileTypeToString(file_type_).c_str(), build_id_.c_str(),
failed_server_count_);
}
if (final_err.has_error()) {
LOGS(Info) << "Download for " << build_id_ << " failed(" << ErrTypeToString(final_err.type())
<< "): " << final_err.msg();
} else {
DEBUG_LOG(DownloadManager) << "Download for " << build_id_ << " succeeded.";
}
if (debug::MessageLoop::Current()) {
debug::MessageLoop::Current()->PostTask(
FROM_HERE, [result_cb = std::move(result_cb_), final_err,
path = std::move(path_)]() mutable { result_cb(final_err, path); });
}
result_cb_ = nullptr;
}
void Download::AddServer(SymbolServer* server) {
if (!result_cb_) {
return;
}
DEBUG_LOG(DownloadManager) << "Adding server " << server->name() << " to download for "
<< build_id_;
SymbolServer::FetchCallback cb = [weak_this = weak_factory_.GetWeakPtr(), server](
const Err& err, const std::string& path) {
if (!weak_this)
return;
weak_this->download_in_progress_ = false;
if (path.empty()) {
if (err.msg().empty()) {
DEBUG_LOG(DownloadManager) << "Error downloading " << weak_this->build_id_ << " from "
<< server->name() << ": " << ErrTypeToString(err.type());
} else {
DEBUG_LOG(DownloadManager)
<< "Error downloading " << weak_this->build_id_ << " from " << server->name() << ": "
<< ErrTypeToString(err.type()) << ", " << err.msg();
}
weak_this->Error(err);
} else {
weak_this->errors_.clear();
weak_this->path_ = path;
weak_this->Finish();
}
};
if (!download_in_progress_) {
download_in_progress_ = true;
server->Fetch(build_id_, file_type_, std::move(cb));
} else {
// It's safe to capture |this| here since |server_fetches_| is owned by
// the Download.
server_fetches_.push_back([server, this, cb = std::move(cb)]() mutable {
server->Fetch(build_id_, file_type_, std::move(cb));
});
}
}
void Download::Error(const Err& err) {
if (!result_cb_)
return;
failed_server_count_++;
errors_[err.type()].push_back(err.msg());
if (!download_in_progress_ && !server_fetches_.empty()) {
auto fetch = std::move(server_fetches_.back());
server_fetches_.pop_back();
download_in_progress_ = true;
fetch();
return;
}
// Tried all available servers, report completion.
Finish();
}
// ============================ DownloadManager ===============================
DownloadManager::DownloadManager(System* system) : system_(system), weak_factory_(this) {
system_->AddObserver(this);
}
DownloadManager::~DownloadManager() { system_->RemoveObserver(this); }
void DownloadManager::RequestDownload(const std::string& build_id, DebugSymbolFileType file_type) {
GetDownload(build_id, file_type);
}
void DownloadManager::DidCreateSymbolServer(SymbolServer* server) {
OnSymbolServerStatusChanged(server);
}
void DownloadManager::OnSymbolServerStatusChanged(SymbolServer* server) {
switch (server->state()) {
case SymbolServer::State::kReady:
OnSymbolServerBecomesReady(server);
break;
case SymbolServer::State::kAuth:
servers_failed_auth_++;
break;
case SymbolServer::State::kUnreachable:
servers_unreachable_++;
break;
default:
break;
}
}
void DownloadManager::OnSymbolServerBecomesReady(SymbolServer* server) {
servers_ready_++;
Download* download = nullptr;
for (const auto& target : system_->GetTargets()) {
auto process = target->GetProcess();
if (!process)
continue;
for (const auto& mod : process->GetSymbols()->GetStatus()) {
if (!mod.symbols || !mod.symbols->module_symbols()) {
download = GetDownload(mod.build_id, DebugSymbolFileType::kDebugInfo);
} else if (!mod.symbols->module_symbols()->HasBinary()) {
download = GetDownload(mod.build_id, DebugSymbolFileType::kBinary);
}
if (download) {
download->AddServer(server);
}
}
}
}
Download* DownloadManager::InjectDownloadForTesting(const std::string& build_id) {
return GetDownload(build_id, DebugSymbolFileType::kDebugInfo);
}
void DownloadManager::AbandonTestingDownload(const std::string& build_id) {
if (const auto& it = downloads_.find({build_id, DebugSymbolFileType::kDebugInfo});
it != downloads_.end()) {
it->second.reset();
}
}
std::unique_ptr<Download> DownloadManager::MakeDownload(const std::string& build_id,
DebugSymbolFileType file_type) {
// |this| owns the Download object, so it's safe to capture in these callbacks.
return std::make_unique<Download>(
build_id, file_type, [build_id, file_type, this](const Err& err, const std::string& path) {
if (err.ok() && !path.empty()) {
OnDownloadSucceeded(path, build_id, file_type);
} else if (servers_ready_ + servers_failed_auth_ < system_->GetSymbolServers().size()) {
// Erase the download and return early if not all servers have reported ready or failed to
// authenticate. We don't want to report an error yet. Once all the servers have reported,
// the download will try again and report the error if none succeed.
downloads_.erase({build_id, file_type});
return;
} else {
download_fail_count_++;
system_->NotifyFailedToFindDebugSymbols(err, build_id, file_type);
}
downloads_.erase({build_id, file_type});
DownloadFinished();
});
}
Download* DownloadManager::GetDownload(std::string build_id, DebugSymbolFileType file_type) {
DownloadIdentifier download_id{build_id, file_type};
Download* download = nullptr;
if (const auto& it = downloads_.find(download_id); it != downloads_.end()) {
return it->second.get();
}
// Avoid starting a download if all symbol servers are in a state where they will
// never become ready. Otherwise, the download could never complete.
if (servers_failed_auth_ + servers_unreachable_ < system_->GetSymbolServers().size()) {
auto unique_download = MakeDownload(build_id, file_type);
download = unique_download.get();
downloads_[download_id] = std::move(unique_download);
// Add all configured servers to the new download object.
DownloadStarted(download_id, download);
} else {
DEBUG_LOG(DownloadManager) << "No symbol servers for download for " << build_id;
}
return download;
}
void DownloadManager::OnDownloadSucceeded(const std::string& path, const std::string& build_id,
DebugSymbolFileType file_type) {
download_success_count_++;
// Adds the file manually since the build_id could already be marked as missing in the
// build_id_index.
system_->GetSymbols()->build_id_index().AddOneFile(path);
for (const auto target : system_->GetTargets()) {
if (auto process = target->GetProcess()) {
// Don't need local symbol lookup when retrying a download, so can pass an empty
// module name.
process->GetSymbols()->RetryLoadBuildID(std::string(), build_id, file_type);
}
}
}
void DownloadManager::DownloadStarted(const DownloadIdentifier& dl_id, Download* download) {
FX_DCHECK(system_);
for (auto& server : system_->GetSymbolServers()) {
if (server->state() == SymbolServer::State::kReady) {
download->AddServer(server);
} else {
DEBUG_LOG(DownloadManager) << "Symbol server " << server->name() << " is not ready.";
}
}
if (download_count_ == 0) {
for (auto& observer : system_->session()->download_observers()) {
observer.OnDownloadsStarted();
}
}
download_count_++;
}
void DownloadManager::DownloadFinished() {
download_count_--;
FX_DCHECK(system_);
if (download_count_ == 0) {
for (auto& observer : system_->session()->download_observers()) {
observer.OnDownloadsStopped(download_success_count_, download_fail_count_);
}
download_success_count_ = download_fail_count_ = 0;
}
}
bool DownloadManager::HasDownload(const std::string& build_id) const {
auto download = downloads_.find({build_id, DebugSymbolFileType::kDebugInfo});
if (download == downloads_.end()) {
download = downloads_.find({build_id, DebugSymbolFileType::kBinary});
}
if (download == downloads_.end()) {
return false;
}
auto ptr = download->second.get();
return ptr;
}
} // namespace zxdb