1use flate2::read::GzDecoder;
2use hyper::body::Bytes;
3use hyper::server::conn::AddrIncoming;
4use hyper::server::Builder;
5use hyper::service::{make_service_fn, service_fn};
6use hyper::{header, Body, Request, Response, Server};
7use hyper::{Method, StatusCode};
8use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
9use rand::RngCore;
10use serde_derive::Deserialize;
11use std::collections::HashMap;
12use std::convert::Infallible;
13use std::ffi::{OsStr, OsString};
14use std::io::BufReader;
15use std::net::SocketAddr;
16use std::ops::Range;
17use std::path::{Path, PathBuf};
18use std::str::FromStr;
19use std::sync::Arc;
20use symsrv::NtSymbolPathEntry;
21use tokio::io::AsyncReadExt;
22use wholesym::debugid::{CodeId, DebugId};
23use wholesym::{LibraryInfo, SymbolManager, SymbolManagerConfig};
24
25pub use symsrv;
26pub use wholesym::samply_symbols;
27
28const BAD_CHARS: &AsciiSet = &CONTROLS.add(b':').add(b'/');
29
30#[test]
31fn test_is_send_and_sync() {
32 use symsrv::FileContents;
33 fn assert_is_send<T: Send>() {}
34 fn assert_is_sync<T: Sync>() {}
35 assert_is_send::<FileContents>();
36 assert_is_sync::<FileContents>();
37}
38
39#[derive(Clone, Debug)]
40pub enum PortSelection {
41 OnePort(u16),
42 TryMultiple(Range<u16>),
43}
44
45impl PortSelection {
46 pub fn try_from_str(s: &str) -> std::result::Result<Self, <u16 as FromStr>::Err> {
47 if s.ends_with('+') {
48 let start = s.trim_end_matches('+').parse()?;
49 let end = start + 100;
50 Ok(PortSelection::TryMultiple(start..end))
51 } else {
52 Ok(PortSelection::OnePort(s.parse()?))
53 }
54 }
55}
56
57pub async fn start_server(
58 profile_filename: Option<&Path>,
59 port_selection: PortSelection,
60 symbol_path: Vec<NtSymbolPathEntry>,
61 verbose: bool,
62 open_in_browser: bool,
63) {
64 let libinfo_map = if let Some(profile_filename) = profile_filename {
65 let file = std::fs::File::open(profile_filename).expect("couldn't read file");
69 let reader = BufReader::new(file);
70
71 if profile_filename.extension() == Some(&OsString::from("gz")) {
73 let decoder = GzDecoder::new(reader);
74 let reader = BufReader::new(decoder);
75 parse_libinfo_map_from_profile(reader).expect("couldn't parse json")
76 } else {
77 parse_libinfo_map_from_profile(reader).expect("couldn't parse json")
78 }
79 } else {
80 HashMap::new()
81 };
82
83 let (builder, addr) = make_builder_at_port(port_selection);
84
85 let token = generate_token();
86 let path_prefix = format!("/{}", token);
87 let server_origin = format!("http://{}", addr);
88 let symbol_server_url = format!("{}{}", server_origin, path_prefix);
89 let mut template_values: HashMap<&'static str, String> = HashMap::new();
90 template_values.insert("SERVER_URL", server_origin.clone());
91 template_values.insert("PATH_PREFIX", path_prefix.clone());
92
93 let profiler_url = if profile_filename.is_some() {
94 let profile_url = format!("{}/profile.json", symbol_server_url);
95
96 let env_profiler_override = std::env::var("PROFILER_URL").ok();
97 let profiler_origin = match &env_profiler_override {
98 Some(s) => s.trim_end_matches('/'),
99 None => "https://profiler.firefox.com",
100 };
101
102 let encoded_profile_url = utf8_percent_encode(&profile_url, BAD_CHARS).to_string();
103 let encoded_symbol_server_url =
104 utf8_percent_encode(&symbol_server_url, BAD_CHARS).to_string();
105 let profiler_url = format!(
106 "{}/from-url/{}/?symbolServer={}",
107 profiler_origin, encoded_profile_url, encoded_symbol_server_url
108 );
109 template_values.insert("PROFILER_URL", profiler_url.clone());
110 template_values.insert("PROFILE_URL", profile_url);
111 Some(profiler_url)
112 } else {
113 None
114 };
115
116 let template_values = Arc::new(template_values);
117
118 let config = SymbolManagerConfig::new()
119 .verbose(verbose)
120 .with_nt_symbol_path(symbol_path);
121 let mut symbol_manager = SymbolManager::with_config(config);
122 for lib_info in libinfo_map.into_values() {
123 symbol_manager.add_known_lib(lib_info);
124 }
125 let symbol_manager = Arc::new(symbol_manager);
126 let new_service = make_service_fn(move |_conn| {
127 let symbol_manager = symbol_manager.clone();
128 let profile_filename = profile_filename.map(PathBuf::from);
129 let template_values = template_values.clone();
130 let path_prefix = path_prefix.clone();
131 async {
132 Ok::<_, Infallible>(service_fn(move |req| {
133 symbolication_service(
134 req,
135 template_values.clone(),
136 symbol_manager.clone(),
137 profile_filename.clone(),
138 path_prefix.clone(),
139 )
140 }))
141 }
142 });
143
144 let server = builder.serve(new_service);
145
146 eprintln!("Local server listening at {}", server_origin);
147 if !open_in_browser {
148 if let Some(profiler_url) = &profiler_url {
149 eprintln!(" Open the profiler at {}", profiler_url);
150 }
151 }
152 eprintln!("Press Ctrl+C to stop.");
153
154 if open_in_browser {
155 if let Some(profiler_url) = &profiler_url {
156 let _ = webbrowser::open(profiler_url);
157 }
158 }
159
160 if let Err(e) = server.await {
162 eprintln!("server error: {}", e);
163 }
164}
165
166fn parse_libinfo_map_from_profile(
167 reader: impl std::io::Read,
168) -> Result<HashMap<(String, DebugId), LibraryInfo>, std::io::Error> {
169 let profile: ProfileJsonProcess = serde_json::from_reader(reader)?;
170 let mut libinfo_map = HashMap::new();
171 add_to_libinfo_map_recursive(&profile, &mut libinfo_map);
172 Ok(libinfo_map)
173}
174
175#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
176#[serde(rename_all = "camelCase")]
177struct ProfileJsonProcess {
178 #[serde(default)]
179 pub libs: Vec<ProfileJsonLib>,
180 #[serde(default)]
181 pub threads: Vec<ProfileJsonThread>,
182 #[serde(default)]
183 pub processes: Vec<ProfileJsonProcess>,
184}
185
186#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
187#[serde(rename_all = "camelCase")]
188struct ProfileJsonThread {
189 #[serde(default)]
190 pub libs: Vec<ProfileJsonLib>,
191}
192
193#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
194#[serde(rename_all = "camelCase")]
195struct ProfileJsonLib {
196 pub debug_name: Option<String>,
197 pub debug_path: Option<String>,
198 pub name: Option<String>,
199 pub path: Option<String>,
200 pub breakpad_id: Option<String>,
201 pub code_id: Option<String>,
202}
203
204fn generate_token() -> String {
206 let mut bytes = [0u8; 24];
207 rand::thread_rng().fill_bytes(&mut bytes);
208 nix_base32::to_nix_base32(&bytes)
209}
210
211fn make_builder_at_port(port_selection: PortSelection) -> (Builder<AddrIncoming>, SocketAddr) {
212 match port_selection {
213 PortSelection::OnePort(port) => {
214 let addr = SocketAddr::from(([127, 0, 0, 1], port));
215 match Server::try_bind(&addr) {
216 Ok(builder) => (builder, addr),
217 Err(e) => {
218 eprintln!("Could not bind to port {}: {}", port, e);
219 std::process::exit(1)
220 }
221 }
222 }
223 PortSelection::TryMultiple(range) => {
224 let mut error = None;
225 for port in range.clone() {
226 let addr = SocketAddr::from(([127, 0, 0, 1], port));
227 match Server::try_bind(&addr) {
228 Ok(builder) => return (builder, addr),
229 Err(e) => {
230 error.get_or_insert(e);
231 }
232 }
233 }
234 match error {
235 Some(error) => {
236 eprintln!(
237 "Could not bind to any port in the range {:?}: {}",
238 range, error,
239 );
240 }
241 None => {
242 eprintln!("Binding failed, port range empty? {:?}", range);
243 }
244 }
245 std::process::exit(1)
246 }
247 }
248}
249
250const TEMPLATE_WITH_PROFILE: &str = r#"
251<!DOCTYPE html>
252<html lang="en">
253<meta charset="utf-8">
254<title>Profiler Symbol Server</title>
255<body>
256
257<p>This is the profiler symbol server, running at <code>SERVER_URL</code>. You can:</p>
258<ul>
259 <li><a href="PROFILER_URL">Open the profile in the profiler UI</a></li>
260 <li><a download href="PROFILE_URL">Download the raw profile JSON</a></li>
261 <li>Obtain symbols by POSTing to <code>PATH_PREFIX/symbolicate/v5</code>, with the format specified by the <a href="https://tecken.readthedocs.io/en/latest/symbolication.html">Mozilla symbolication API documentation</a>.</li>
262 <li>Obtain source code by POSTing to <code>PATH_PREFIX/source/v1</code>, with the format specified in this <a href="https://github.com/mstange/profiler-get-symbols/issues/24#issuecomment-989985588">github comment</a>.</li>
263</ul>
264"#;
265
266const TEMPLATE_WITHOUT_PROFILE: &str = r#"
267<!DOCTYPE html>
268<html lang="en">
269<meta charset="utf-8">
270<title>Profiler Symbol Server</title>
271<body>
272
273<p>This is the profiler symbol server, running at <code>SERVER_URL</code>. You can:</p>
274<ul>
275 <li>Obtain symbols by POSTing to <code>PATH_PREFIX/symbolicate/v5</code>, with the format specified by the <a href="https://tecken.readthedocs.io/en/latest/symbolication.html">Mozilla symbolication API documentation</a>.</li>
276 <li>Obtain source code by POSTing to <code>PATH_PREFIX/source/v1</code>, with the format specified in this <a href="https://github.com/mstange/profiler-get-symbols/issues/24#issuecomment-989985588">github comment</a>.</li>
277</ul>
278"#;
279
280async fn symbolication_service(
281 req: Request<Body>,
282 template_values: Arc<HashMap<&'static str, String>>,
283 symbol_manager: Arc<SymbolManager>,
284 profile_filename: Option<PathBuf>,
285 path_prefix: String,
286) -> Result<Response<Body>, hyper::Error> {
287 let has_profile = profile_filename.is_some();
288 let method = req.method();
289 let path = req.uri().path();
290 let mut response = Response::new(Body::empty());
291
292 let path_without_prefix = match path.strip_prefix(&path_prefix) {
293 None => {
294 match (method, path) {
296 (&Method::GET, "/") => {
297 response.headers_mut().insert(
298 header::CONTENT_TYPE,
299 header::HeaderValue::from_static("text/html"),
300 );
301 let template = match has_profile {
302 true => TEMPLATE_WITH_PROFILE,
303 false => TEMPLATE_WITHOUT_PROFILE,
304 };
305 *response.body_mut() =
306 Body::from(substitute_template(template, &template_values));
307 }
308 _ => {
309 *response.status_mut() = StatusCode::NOT_FOUND;
310 }
311 }
312 return Ok(response);
313 }
314 Some(path_without_prefix) => path_without_prefix,
315 };
316
317 response.headers_mut().insert(
322 header::ACCESS_CONTROL_ALLOW_ORIGIN,
323 header::HeaderValue::from_static("*"),
324 );
325
326 match (method, path_without_prefix, profile_filename) {
327 (&Method::OPTIONS, _, _) => {
328 *response.status_mut() = StatusCode::NO_CONTENT;
330 if req
331 .headers()
332 .contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
333 {
334 response.headers_mut().insert(
337 header::ACCESS_CONTROL_ALLOW_METHODS,
338 header::HeaderValue::from_static("POST, GET, OPTIONS"),
339 );
340 response.headers_mut().insert(
341 header::ACCESS_CONTROL_MAX_AGE,
342 header::HeaderValue::from(86400),
343 );
344 if let Some(req_headers) = req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS)
345 {
346 response
348 .headers_mut()
349 .insert(header::ACCESS_CONTROL_ALLOW_HEADERS, req_headers.clone());
350 }
351 } else {
352 response.headers_mut().insert(
354 header::ALLOW,
355 header::HeaderValue::from_static("POST, GET, OPTIONS"),
356 );
357 }
358 }
359 (&Method::GET, "/profile.json", Some(profile_filename)) => {
360 if profile_filename.extension() == Some(OsStr::new("gz")) {
361 response.headers_mut().insert(
362 header::CONTENT_ENCODING,
363 header::HeaderValue::from_static("gzip"),
364 );
365 }
366 response.headers_mut().insert(
367 header::CONTENT_TYPE,
368 header::HeaderValue::from_static("application/json; charset=UTF-8"),
369 );
370 let (mut sender, body) = Body::channel();
371 *response.body_mut() = body;
372
373 tokio::spawn(async move {
375 let mut file = tokio::fs::File::open(&profile_filename)
376 .await
377 .expect("couldn't open profile file");
378 let mut contents = vec![0; 1024 * 1024];
379 loop {
380 let data_len = file
381 .read(&mut contents)
382 .await
383 .expect("couldn't read profile file");
384 if data_len == 0 {
385 break;
386 }
387 sender
388 .send_data(Bytes::copy_from_slice(&contents[..data_len]))
389 .await
390 .expect("couldn't send data");
391 }
392 });
393 }
394 (&Method::POST, path, _) => {
395 response.headers_mut().insert(
396 header::CONTENT_TYPE,
397 header::HeaderValue::from_static("application/json"),
398 );
399 let path = path.to_string();
400 let full_body = hyper::body::to_bytes(req.into_body()).await?;
402 let full_body = String::from_utf8(full_body.to_vec()).expect("invalid utf-8");
403 let response_json = symbol_manager.query_json_api(&path, &full_body).await;
404
405 *response.body_mut() = response_json.into();
406 }
407 _ => {
408 *response.status_mut() = StatusCode::NOT_FOUND;
409 }
410 };
411
412 Ok(response)
413}
414
415fn substitute_template(template: &str, template_values: &HashMap<&'static str, String>) -> String {
416 let mut s = template.to_string();
417 for (key, value) in template_values {
418 s = s.replace(key, value);
419 }
420 s
421}
422
423fn add_libs_to_libinfo_map(
424 libs: &[ProfileJsonLib],
425 libinfo_map: &mut HashMap<(String, DebugId), LibraryInfo>,
426) {
427 for lib in libs {
428 if let Some(lib_info) = libinfo_map_entry_for_lib(lib) {
429 libinfo_map.insert((lib_info.debug_name.clone(), lib_info.debug_id), lib_info);
430 }
431 }
432}
433
434fn libinfo_map_entry_for_lib(lib: &ProfileJsonLib) -> Option<LibraryInfo> {
435 let debug_name = lib.debug_name.clone()?;
436 let breakpad_id = lib.breakpad_id.as_ref()?;
437 let debug_path = lib.debug_path.clone();
438 let name = lib.name.clone();
439 let path = lib.path.clone();
440 let debug_id = DebugId::from_breakpad(breakpad_id).ok()?;
441 let code_id = lib
442 .code_id
443 .as_deref()
444 .and_then(|ci| CodeId::from_str(ci).ok());
445 let lib_info = LibraryInfo {
446 debug_id,
447 debug_name,
448 debug_path,
449 name,
450 code_id,
451 path,
452 };
453 Some(lib_info)
454}
455
456fn add_to_libinfo_map_recursive(
457 profile: &ProfileJsonProcess,
458 libinfo_map: &mut HashMap<(String, DebugId), LibraryInfo>,
459) {
460 add_libs_to_libinfo_map(&profile.libs, libinfo_map);
461 for thread in &profile.threads {
462 add_libs_to_libinfo_map(&thread.libs, libinfo_map);
463 }
464 for process in &profile.processes {
465 add_to_libinfo_map_recursive(process, libinfo_map);
466 }
467}
468
469#[cfg(test)]
470mod test {
471 use crate::{ProfileJsonLib, ProfileJsonProcess};
472
473 #[test]
474 fn deserialize_profile_json() {
475 let p: ProfileJsonProcess = serde_json::from_str("{}").unwrap();
476 assert!(p.libs.is_empty());
477 assert!(p.threads.is_empty());
478 assert!(p.processes.is_empty());
479
480 let p: ProfileJsonProcess = serde_json::from_str("{\"unknown_field\":[1, 2, 3]}").unwrap();
481 assert!(p.libs.is_empty());
482 assert!(p.threads.is_empty());
483 assert!(p.processes.is_empty());
484
485 let p: ProfileJsonProcess =
486 serde_json::from_str("{\"threads\":[{\"libs\":[{}]}]}").unwrap();
487 assert!(p.libs.is_empty());
488 assert_eq!(p.threads.len(), 1);
489 assert_eq!(p.threads[0].libs.len(), 1);
490 assert_eq!(p.threads[0].libs[0], ProfileJsonLib::default());
491 assert!(p.processes.is_empty());
492 }
493}