use clap::Parser;
use indicatif::{DecimalBytes, MultiProgress, ProgressBar};
use symsrv::{
get_home_sym_dir, parse_nt_symbol_path, CabExtractionError, CachePath, DownloadError,
NtSymbolPathEntry, SymsrvDownloader, SymsrvObserver,
};
use std::collections::HashMap;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Parser)]
#[clap(
version,
about = "Fetch a symbol file.",
long_about = "
Fetch a single symbol file from a symbol server, or find it in a local cache directory.
Prints the local path to the file to stdout.
Supports symbol servers which serve files with cab compression; the local file will be
decompressed if necessary, and the printed path always refers to a decompressed file.",
override_usage = r#"symfetch [OPTIONS] <name> <hash>
Examples:
symfetch --server "https://msdl.microsoft.com/download/symbols/" --cache ~/sym winmine.exe 3B7D847520000
symfetch --server "https://msdl.microsoft.com/download/symbols/" --cache ~/sym combase.pdb 071849A7C75FD246A3367704EE1CA85B1
symfetch --server "https://renderdoc.org/symbols" --cache ~/sym renderdoc.pdb 6D1DFFC4DC524537962CCABC000820641
symfetch --symbol-path "srv**https://renderdoc.org/symbols" renderdoc.pdb 6D1DFFC4DC524537962CCABC000820641
_NT_SYMBOL_PATH="srv**https://msdl.microsoft.com/download/symbols/*https://chromium-browser-symsrv.commondatastorage.googleapis.com/" symfetch --use-env-symbol-path chrome.dll.pdb 93B17FC546DE07D14C4C44205044422E1"#
)]
struct Args {
#[clap(long)]
server: Vec<String>,
#[clap(long)]
cache: Option<PathBuf>,
#[clap(long)]
quiet: bool,
#[clap(long, short = 'e')]
use_env_symbol_path: bool,
#[clap(long)]
symbol_path: Option<String>,
#[clap(long)]
default_downstream_store: Option<PathBuf>,
name: String,
hash: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args: Args = Args::parse();
let mut parsed_nt_symbol_path = Vec::new();
let nt_symbol_path = if args.use_env_symbol_path {
std::env::var("_NT_SYMBOL_PATH").ok()
} else {
args.symbol_path
};
if let Some(nt_symbol_path) = nt_symbol_path {
parsed_nt_symbol_path.extend(parse_nt_symbol_path(&nt_symbol_path));
}
if !args.server.is_empty() {
let mut cache_paths = Vec::new();
if let Some(cache) = args.cache {
cache_paths.push(CachePath::Path(cache));
}
let urls = args.server;
parsed_nt_symbol_path.push(NtSymbolPathEntry::Chain {
dll: "symsrv.dll".into(),
cache_paths,
urls,
});
}
let observer = Arc::new(SymFetchObserver::new());
let mut downloader = SymsrvDownloader::new(parsed_nt_symbol_path);
downloader.set_default_downstream_store(get_home_sym_dir());
downloader.set_observer(Some(observer));
let path = downloader.get_file(&args.name, &args.hash).await?;
println!("{}", path.to_string_lossy());
Ok(())
}
struct SymFetchObserver {
inner: Mutex<SymFetchObserverInner>,
}
impl SymFetchObserver {
pub fn new() -> Self {
Self {
inner: Mutex::new(SymFetchObserverInner::new()),
}
}
fn get_inner(&self) -> MutexGuard<SymFetchObserverInner> {
self.inner.lock().unwrap()
}
}
impl SymsrvObserver for SymFetchObserver {
fn on_new_download_before_connect(&self, download_id: u64, url: &str) {
self.get_inner()
.on_new_download_before_connect(download_id, url);
}
fn on_download_failed(&self, download_id: u64, error: DownloadError) {
self.get_inner().on_download_failed(download_id, error);
}
fn on_download_canceled(&self, download_id: u64) {
self.get_inner().on_download_canceled(download_id);
}
fn on_download_started(&self, download_id: u64) {
self.get_inner().on_download_started(download_id);
}
fn on_download_progress(&self, download_id: u64, bytes_so_far: u64, total_bytes: Option<u64>) {
self.get_inner()
.on_download_progress(download_id, bytes_so_far, total_bytes);
}
fn on_download_completed(
&self,
download_id: u64,
uncompressed_size_in_bytes: u64,
_time_until_headers: std::time::Duration,
_time_until_completed: std::time::Duration,
) {
self.get_inner()
.on_download_completed(download_id, uncompressed_size_in_bytes);
}
fn on_new_cab_extraction(&self, extraction_id: u64, dest_path: &Path) {
self.get_inner()
.on_new_cab_extraction(extraction_id, dest_path);
}
fn on_cab_extraction_progress(&self, extraction_id: u64, bytes_so_far: u64, total_bytes: u64) {
self.get_inner()
.on_cab_extraction_progress(extraction_id, bytes_so_far, total_bytes);
}
fn on_cab_extraction_completed(
&self,
extraction_id: u64,
uncompressed_size_in_bytes: u64,
_time_until_completed: std::time::Duration,
) {
self.get_inner()
.on_cab_extraction_completed(extraction_id, uncompressed_size_in_bytes);
}
fn on_cab_extraction_failed(&self, extraction_id: u64, reason: CabExtractionError) {
self.get_inner()
.on_cab_extraction_failed(extraction_id, reason);
}
fn on_cab_extraction_canceled(&self, extraction_id: u64) {
self.get_inner().on_cab_extraction_canceled(extraction_id);
}
fn on_file_missed(&self, _path: &Path) {}
fn on_file_created(&self, _path: &Path, _size_in_bytes: u64) {}
fn on_file_accessed(&self, _path: &Path) {}
}
struct SymFetchObserverInner {
multi_progress: MultiProgress,
requests: HashMap<u64, RequestData>,
extractions: HashMap<u64, ExtractionData>,
}
impl SymFetchObserverInner {
pub fn new() -> Self {
Self {
multi_progress: MultiProgress::new(),
requests: HashMap::new(),
extractions: HashMap::new(),
}
}
fn on_new_download_before_connect(&mut self, download_id: u64, url: &str) {
let progress_bar = self.multi_progress.add(ProgressBar::new_spinner());
progress_bar.set_style(style::spinner());
progress_bar.set_message(format!("Connecting to {url}..."));
self.requests.insert(
download_id,
RequestData {
progress_bar,
url: url.to_owned(),
is_determinate: false,
},
);
}
fn on_download_failed(&mut self, download_id: u64, error: DownloadError) {
let request = self.requests.remove(&download_id).unwrap();
request.progress_bar.finish_and_clear();
self.multi_progress.remove(&request.progress_bar);
let url = request.url;
self.multi_progress
.println(format!("Request to {url} failed: {error}"))
.unwrap();
}
fn on_download_canceled(&mut self, download_id: u64) {
let request = self.requests.remove(&download_id).unwrap();
request.progress_bar.finish_and_clear();
self.multi_progress.remove(&request.progress_bar);
let url = request.url;
self.multi_progress
.println(format!("Canceled request to {url}."))
.unwrap();
}
fn message_for_url(url: &str) -> String {
format!("Downloading from {url}...")
}
fn on_download_started(&mut self, download_id: u64) {
let request = self.requests.get_mut(&download_id).unwrap();
let message = Self::message_for_url(&request.url);
request.progress_bar.set_message(message);
}
fn on_download_progress(
&mut self,
download_id: u64,
bytes_so_far: u64,
total_bytes: Option<u64>,
) {
let request = self.requests.get_mut(&download_id).unwrap();
match (request.is_determinate, total_bytes) {
(false, Some(total_bytes)) => {
let progress_bar = self.multi_progress.insert_after(
&request.progress_bar,
ProgressBar::new(total_bytes).with_elapsed(request.progress_bar.elapsed()),
);
self.multi_progress.remove(&request.progress_bar);
progress_bar.set_style(style::bar());
progress_bar.set_message(Self::message_for_url(&request.url));
request.progress_bar = progress_bar;
request.is_determinate = true;
}
(true, None) => {
let progress_bar = self.multi_progress.insert_after(
&request.progress_bar,
ProgressBar::new_spinner().with_elapsed(request.progress_bar.elapsed()),
);
self.multi_progress.remove(&request.progress_bar);
progress_bar.set_style(style::spinner());
progress_bar.set_message(Self::message_for_url(&request.url));
request.progress_bar = progress_bar;
request.is_determinate = false;
}
_ => {}
}
request.progress_bar.set_position(bytes_so_far);
}
fn on_download_completed(&mut self, download_id: u64, uncompressed_size_in_bytes: u64) {
let request = self.requests.remove(&download_id).unwrap();
request.progress_bar.finish();
self.multi_progress.remove(&request.progress_bar);
let url = request.url;
self.multi_progress
.println(format!(
"Successfully downloaded {} from {url}.",
DecimalBytes(uncompressed_size_in_bytes)
))
.unwrap();
}
fn on_new_cab_extraction(&mut self, extraction_id: u64, dest_path: &Path) {
let progress_bar = self.multi_progress.add(ProgressBar::new_spinner());
progress_bar.set_style(style::spinner());
progress_bar.set_message(format!(
"Extracting to {dest_path}...",
dest_path = dest_path.to_string_lossy()
));
self.extractions.insert(
extraction_id,
ExtractionData {
progress_bar,
extracted_path: dest_path.to_owned(),
is_determinate: false,
},
);
}
fn on_cab_extraction_progress(
&mut self,
extraction_id: u64,
bytes_so_far: u64,
total_bytes: u64,
) {
let extraction = self.extractions.get_mut(&extraction_id).unwrap();
if !extraction.is_determinate {
let progress_bar = self.multi_progress.insert_after(
&extraction.progress_bar,
ProgressBar::new(total_bytes).with_elapsed(extraction.progress_bar.elapsed()),
);
self.multi_progress.remove(&extraction.progress_bar);
progress_bar.set_style(style::bar());
progress_bar.set_message(format!(
"Extracting {path}...",
path = extraction.extracted_path.to_string_lossy()
));
extraction.progress_bar = progress_bar;
extraction.is_determinate = true;
}
extraction.progress_bar.set_position(bytes_so_far);
}
fn on_cab_extraction_completed(&mut self, extraction_id: u64, uncompressed_size_in_bytes: u64) {
let extraction = self.extractions.remove(&extraction_id).unwrap();
extraction.progress_bar.finish();
self.multi_progress.remove(&extraction.progress_bar);
self.multi_progress
.println(format!(
"Successfully extracted {} to {path}.",
DecimalBytes(uncompressed_size_in_bytes),
path = extraction.extracted_path.to_string_lossy()
))
.unwrap();
}
fn on_cab_extraction_failed(&mut self, extraction_id: u64, reason: CabExtractionError) {
let extraction = self.extractions.remove(&extraction_id).unwrap();
extraction.progress_bar.finish_and_clear();
self.multi_progress.remove(&extraction.progress_bar);
self.multi_progress
.println(format!(
"Failed to extract {path}: {reason}.",
path = extraction.extracted_path.to_string_lossy(),
reason = reason
))
.unwrap();
}
fn on_cab_extraction_canceled(&mut self, extraction_id: u64) {
let extraction = self.extractions.remove(&extraction_id).unwrap();
extraction.progress_bar.finish_and_clear();
self.multi_progress.remove(&extraction.progress_bar);
self.multi_progress
.println(format!(
"Canceled extraction of {path}.",
path = extraction.extracted_path.to_string_lossy()
))
.unwrap();
}
}
struct RequestData {
progress_bar: ProgressBar,
url: String,
is_determinate: bool,
}
struct ExtractionData {
progress_bar: ProgressBar,
extracted_path: PathBuf,
is_determinate: bool,
}
mod style {
use indicatif::ProgressStyle;
pub fn bar() -> ProgressStyle {
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}] {bar:.cyan/blue} {decimal_bytes:>12}/{decimal_total_bytes:12} {wide_msg}",
)
.unwrap()
.progress_chars("█▉▊▋▌▍▎▏ ")
}
pub fn spinner() -> ProgressStyle {
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {spinner} {bytes_per_sec:>10} {wide_msg}")
.unwrap()
}
}