Crash page (#644)

* Uses agb_debug in wasm to show addresses and more with additional
debug information!
This commit is contained in:
Corwin 2024-04-20 22:42:22 +01:00 committed by GitHub
commit 8c8581e19c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 617 additions and 145 deletions

View file

@ -33,6 +33,8 @@ jobs:
uses: peaceiris/actions-mdbook@v2 uses: peaceiris/actions-mdbook@v2
with: with:
mdbook-version: "0.4.13" mdbook-version: "0.4.13"
- name: Setup wasm
run: just setup-cargo-wasm
- name: Build website - name: Build website
run: just build-site run: just build-site
- name: Upload artifact - name: Upload artifact

View file

@ -26,6 +26,7 @@ members = [
"emulator/mgba", "emulator/mgba",
"emulator/mgba-sys", "emulator/mgba-sys",
"emulator/test-runner", "emulator/test-runner",
"website/backtrace",
] ]
exclude = [ exclude = [

View file

@ -8,7 +8,7 @@ description = "CLI utility to convert agb stack trace dumps into human readable
repository = "https://github.com/agbrs/agb" repository = "https://github.com/agbrs/agb"
[dependencies] [dependencies]
anyhow = "1" thiserror = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
addr2line = { version = "0.22", default-features = false, features = [ addr2line = { version = "0.22", default-features = false, features = [
"rustc-demangle", "rustc-demangle",

View file

@ -1,8 +1,20 @@
use std::{slice::ChunksExact, sync::OnceLock}; use std::{slice::ChunksExact, sync::OnceLock};
use thiserror::Error;
const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
pub fn gwilym_decode(input: &str) -> anyhow::Result<GwilymDecodeIter<'_>> { #[derive(Debug, Error)]
pub enum GwilymDecodeError {
#[error("Does not contain version")]
NoVersion,
#[error("Only version 1 is supported")]
WrongVersion,
#[error("Input must be a multiple of 3 but have {0}")]
LengthWrong(usize),
}
pub fn gwilym_decode(input: &str) -> Result<GwilymDecodeIter<'_>, GwilymDecodeError> {
GwilymDecodeIter::new(input) GwilymDecodeIter::new(input)
} }
@ -11,21 +23,21 @@ pub struct GwilymDecodeIter<'a> {
} }
impl<'a> GwilymDecodeIter<'a> { impl<'a> GwilymDecodeIter<'a> {
fn new(input: &'a str) -> anyhow::Result<Self> { fn new(input: &'a str) -> Result<Self, GwilymDecodeError> {
let input = input let input = input
.strip_prefix("https://agbrs.dev/crash#") .strip_prefix("https://agbrs.dev/crash#")
.unwrap_or(input); .unwrap_or(input);
let Some((input, version)) = input.rsplit_once('v') else { let Some((input, version)) = input.rsplit_once('v') else {
anyhow::bail!("Does not contain version"); return Err(GwilymDecodeError::NoVersion);
}; };
if version != "1" { if version != "1" {
anyhow::bail!("Only version 1 is supported"); return Err(GwilymDecodeError::WrongVersion);
} }
if input.len() % 3 != 0 { if input.len() % 3 != 0 {
anyhow::bail!("Input string must have length a multiple of 3"); return Err(GwilymDecodeError::LengthWrong(input.len()));
} }
Ok(Self { Ok(Self {
@ -75,11 +87,11 @@ fn get_value_for_char(input: u8) -> u32 {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{gwilym_decode, ALPHABET}; use super::*;
use std::fmt::Write; use std::fmt::Write;
#[test] #[test]
fn should_correctly_decode_16s() -> anyhow::Result<()> { fn should_correctly_decode_16s() -> Result<(), GwilymDecodeError> {
assert_eq!( assert_eq!(
&gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(), &gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(),
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
@ -112,7 +124,7 @@ mod test {
} }
#[test] #[test]
fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> { fn should_correctly_decode_16s_and_32s() -> Result<(), Box<dyn std::error::Error>> {
let trace: &[u32] = &[ let trace: &[u32] = &[
0x0300_2990, 0x0300_2990,
0x0800_3289, 0x0800_3289,
@ -143,7 +155,7 @@ mod test {
} }
#[test] #[test]
fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> { fn should_strip_the_agbrsdev_prefix() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!( assert_eq!(
&gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(), &gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(),
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]

93
agb-debug/src/lib.rs Normal file
View file

@ -0,0 +1,93 @@
mod gwilym_encoding;
mod load_dwarf;
use addr2line::gimli;
pub use gwilym_encoding::{gwilym_decode, GwilymDecodeError};
pub use load_dwarf::{load_dwarf, GimliDwarf, LoadDwarfError};
use thiserror::Error;
pub use addr2line;
pub struct AddressInfo {
pub location: Location,
pub is_interesting: bool,
pub is_inline: bool,
pub function: String,
}
#[derive(Debug, Error)]
pub enum AddressInfoError {
#[error(transparent)]
Gimli(#[from] gimli::Error),
}
pub struct Location {
pub filename: String,
pub line: u32,
pub col: u32,
}
pub type Addr2LineContext = addr2line::Context<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
impl Default for Location {
fn default() -> Self {
Self {
filename: "??".to_string(),
line: 0,
col: 0,
}
}
}
pub fn address_info(
ctx: &Addr2LineContext,
address: u64,
) -> Result<Vec<AddressInfo>, AddressInfoError> {
let mut frames = ctx.find_frames(address).skip_all_loads()?;
let mut is_first = true;
let mut infos = Vec::new();
while let Some(frame) = frames.next()? {
let function_name = if let Some(ref func) = frame.function {
func.demangle()?.into_owned()
} else {
"unknown function".to_string()
};
let location = frame
.location
.as_ref()
.map(|location| Location {
filename: location.file.unwrap_or("??").to_owned(),
line: location.line.unwrap_or(0),
col: location.column.unwrap_or(0),
})
.unwrap_or_default();
let is_interesting = is_interesting_function(&function_name, &location.filename);
infos.push(AddressInfo {
location,
is_interesting,
is_inline: !is_first,
function: function_name,
});
is_first = false;
}
Ok(infos)
}
fn is_interesting_function(function_name: &str, path: &str) -> bool {
if function_name == "rust_begin_unwind" {
return false; // this is the unwind exception call
}
if path.ends_with("panicking.rs") {
return false; // probably part of rust's internal panic mechanisms
}
true
}

View file

@ -4,38 +4,52 @@ use addr2line::{
gimli, gimli,
object::{self, Object}, object::{self, Object},
}; };
use anyhow::bail; use thiserror::Error;
pub fn load_dwarf( #[derive(Debug, Error)]
file_content: &[u8], pub enum LoadDwarfError {
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> { #[error("Gba file is empty")]
GbaFileEmpty,
#[error("Failed to load debug information from ROM file, it might not have been included?")]
NoDebugInformation,
#[error("Failed to load debug information: {0}")]
DeserializationError(#[from] rmp_serde::decode::Error),
#[error(transparent)]
GimliError(#[from] gimli::Error),
}
pub type GimliDwarf = gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
pub fn load_dwarf(file_content: &[u8]) -> Result<GimliDwarf, LoadDwarfError> {
if let Ok(object) = object::File::parse(file_content) { if let Ok(object) = object::File::parse(file_content) {
return load_from_object(&object); return Ok(load_from_object(&object)?);
} }
// the file might have been padded, so ensure we skip any padding before continuing // the file might have been padded, so ensure we skip any padding before continuing
let last_non_zero_byte = file_content let last_non_zero_byte = file_content
.iter() .iter()
.rposition(|&b| b != 0) .rposition(|&b| b != 0)
.ok_or_else(|| anyhow::anyhow!("Gba file is empty"))?; .ok_or_else(|| LoadDwarfError::GbaFileEmpty)?;
let file_content = &file_content[..last_non_zero_byte + 1]; let file_content = &file_content[..last_non_zero_byte + 1];
let last_8_bytes = &file_content[file_content.len() - 8..]; let last_8_bytes = &file_content[file_content.len() - 8..];
let len = u32::from_le_bytes(last_8_bytes[0..4].try_into()?) as usize; let len = u32::from_le_bytes(
last_8_bytes[0..4]
.try_into()
.or(Err(LoadDwarfError::NoDebugInformation))?,
) as usize;
let version = &last_8_bytes[4..]; let version = &last_8_bytes[4..];
if version != b"agb1" { if version != b"agb1" {
bail!("Failed to load debug information from ROM file, it might not have been included?"); return Err(LoadDwarfError::NoDebugInformation);
} }
let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8]; let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8];
let decompressing_reader = let decompressing_reader =
lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data)); lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data));
let debug_info: HashMap<String, Vec<u8>> = let debug_info: HashMap<String, Vec<u8>> = rmp_serde::decode::from_read(decompressing_reader)?;
rmp_serde::decode::from_read(decompressing_reader)
.map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?;
let dwarf = gimli::Dwarf::load(|id| { let dwarf = gimli::Dwarf::load(|id| {
let data = debug_info let data = debug_info
@ -54,7 +68,7 @@ pub fn load_dwarf(
fn load_from_object<'file>( fn load_from_object<'file>(
object: &object::File<'file, &'file [u8]>, object: &object::File<'file, &'file [u8]>,
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> { ) -> Result<GimliDwarf, gimli::Error> {
let endian = if object.is_little_endian() { let endian = if object.is_little_endian() {
gimli::RunTimeEndian::Little gimli::RunTimeEndian::Little
} else { } else {

View file

@ -1,18 +1,15 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error,
fs::{self, File}, fs::{self, File},
io::Read, io::Read,
path::PathBuf, path::PathBuf,
time::SystemTime, time::SystemTime,
}; };
use addr2line::gimli; use agb_debug::{address_info, AddressInfo, Location};
use clap::Parser; use clap::Parser;
use colored::Colorize; use colored::Colorize;
use load_dwarf::load_dwarf;
mod gwilym_encoding;
mod load_dwarf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -24,23 +21,7 @@ struct Args {
dump: String, dump: String,
} }
struct Location { fn main() -> Result<(), Box<dyn Error>> {
filename: String,
line: u32,
col: u32,
}
impl Default for Location {
fn default() -> Self {
Self {
filename: "??".to_string(),
line: 0,
col: 0,
}
}
}
fn main() -> anyhow::Result<()> {
let cli = Args::parse(); let cli = Args::parse();
let modification_time = fs::metadata(&cli.elf_path)? let modification_time = fs::metadata(&cli.elf_path)?
@ -48,52 +29,28 @@ fn main() -> anyhow::Result<()> {
.unwrap_or(SystemTime::UNIX_EPOCH); .unwrap_or(SystemTime::UNIX_EPOCH);
let file = fs::read(&cli.elf_path)?; let file = fs::read(&cli.elf_path)?;
let dwarf = load_dwarf(&file)?; let dwarf = agb_debug::load_dwarf(&file)?;
let ctx = addr2line::Context::from_dwarf(dwarf)?; let ctx = addr2line::Context::from_dwarf(dwarf)?;
for (i, address) in gwilym_encoding::gwilym_decode(&cli.dump)?.enumerate() { for (i, address) in agb_debug::gwilym_decode(&cli.dump)?.enumerate() {
print_address(&ctx, i, address.into(), modification_time)?; let infos = address_info(&ctx, address.into())?;
for info in infos {
print_address_info(&info, i, modification_time)?;
}
} }
Ok(()) Ok(())
} }
fn print_address( fn print_address_info(
ctx: &addr2line::Context<impl gimli::Reader>, info: &AddressInfo,
index: usize, index: usize,
address: u64,
elf_modification_time: SystemTime, elf_modification_time: SystemTime,
) -> anyhow::Result<()> { ) -> Result<(), Box<dyn Error>> {
let mut frames = ctx.find_frames(address).skip_all_loads()?; let function_name_to_print = &info.function;
let mut is_first = true; if !info.is_inline {
while let Some(frame) = frames.next()? {
let function_name = if let Some(ref func) = frame.function {
func.demangle()?.into_owned()
} else {
"unknown function".to_string()
};
let location = frame
.location
.as_ref()
.map(|location| Location {
filename: location.file.unwrap_or("??").to_owned(),
line: location.line.unwrap_or(0),
col: location.column.unwrap_or(0),
})
.unwrap_or_default();
let is_interesting = is_interesting_function(&function_name, &location.filename);
let function_name_to_print = if is_interesting {
function_name.bold()
} else {
function_name.normal()
};
if is_first {
print!("{index}:\t{function_name_to_print}"); print!("{index}:\t{function_name_to_print}");
} else { } else {
print!("\t(inlined into) {function_name_to_print}"); print!("\t(inlined into) {function_name_to_print}");
@ -101,29 +58,22 @@ fn print_address(
println!( println!(
" {}:{}", " {}:{}",
prettify_path(&location.filename).green(), prettify_path(&info.location.filename).green(),
location.line.to_string().green() info.location.line.to_string().green()
); );
if location.line != 0 && is_interesting { if info.location.line != 0 && info.is_interesting {
print_line_of_code(&frame, location, elf_modification_time)?; print_line_of_code(&info.location, elf_modification_time)?;
}
is_first = false;
} }
Ok(()) Ok(())
} }
fn print_line_of_code( fn print_line_of_code(
frame: &addr2line::Frame<'_, impl gimli::Reader>, location: &Location,
location: Location,
elf_modification_time: SystemTime, elf_modification_time: SystemTime,
) -> anyhow::Result<()> { ) -> Result<(), Box<dyn Error>> {
let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else { let filename = &location.filename;
return Ok(());
};
let Ok(mut file) = File::open(filename) else { let Ok(mut file) = File::open(filename) else {
return Ok(()); return Ok(());
}; };
@ -171,15 +121,3 @@ fn prettify_path(path: &str) -> Cow<'_, str> {
Cow::Borrowed(path) Cow::Borrowed(path)
} }
} }
fn is_interesting_function(function_name: &str, path: &str) -> bool {
if function_name == "rust_begin_unwind" {
return false; // this is the unwind exception call
}
if path.ends_with("panicking.rs") {
return false; // probably part of rust's internal panic mechanisms
}
true
}

View file

@ -87,6 +87,15 @@ release +args: (_run-tool "release" args)
miri: miri:
(cd agb-hashmap && cargo miri test) (cd agb-hashmap && cargo miri test)
setup-cargo-wasm:
cargo install wasm-pack
build-website-backtrace:
(cd website/backtrace && wasm-pack build --target web)
rm -rf website/agb/src/app/vendor/backtrace
mkdir -p website/agb/src/app/vendor
cp website/backtrace/pkg website/agb/src/app/vendor/backtrace -r
build-mgba-wasm: build-mgba-wasm:
rm -rf website/agb/src/app/mgba/vendor rm -rf website/agb/src/app/mgba/vendor
mkdir website/agb/src/app/mgba/vendor mkdir website/agb/src/app/mgba/vendor
@ -96,10 +105,16 @@ build-combo-rom-site:
just _build-rom "examples/combo" "AGBGAMES" just _build-rom "examples/combo" "AGBGAMES"
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz
build-site-app: build-mgba-wasm build-combo-rom-site
setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace
(cd website/agb && npm install --no-save --prefer-offline --no-audit) (cd website/agb && npm install --no-save --prefer-offline --no-audit)
build-site-app: setup-app-build
(cd website/agb && npm run build) (cd website/agb && npm run build)
serve-site-dev: setup-app-build
(cd website/agb && npm run dev)
build-site: build-site-app build-book build-site: build-site-app build-book
rm -rf website/build rm -rf website/build
cp website/agb/out website/build -r cp website/agb/out website/build -r

View file

@ -1,33 +1,93 @@
"use client"; "use client";
import { FC } from "react"; import { useEffect, useState } from "react";
import { useClientValue } from "../useClientValue.hook";
import { ContentBlock } from "../contentBlock";
import { GameDeveloperSummary } from "./gameDeveloperSummary";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { Debug } from "./debug";
const BacktraceWrapper = styled.section` export function BacktracePage() {
display: flex; const [backtrace, setBacktrace] = useState("");
gap: 10px; useEffect(() => {
justify-content: center; setBacktrace(getBacktrace());
`; }, []);
function getBacktrace() {
return window.location.hash.slice(1);
}
export function BacktraceDisplay() {
const backtrace = useClientValue(getBacktrace) ?? "";
return (
<ContentBlock>
<h1>agbrs crash backtrace viewer</h1>
<p>
You likely got here from the link / QR code that was displayed when a
game you were playing crashed. This is the default crash page for games
made using the agb library.
</p>
<p>
The creator of the game is <em>very</em> likely interested in the code
below <em>along with</em> a description of what you were doing at the
time.{" "}
<strong>Send these to the creator of the game you are playing.</strong>
</p>
<BacktraceCopyDisplay backtrace={backtrace} setBacktrace={setBacktrace} />
<p>
<em>
The owners of this website are not necessarily the creators of the
game you are playing.
</em>
</p>
<h2>Backtrace</h2>
{backtrace && <Debug encodedBacktrace={backtrace} />}
<GameDeveloperSummary />
</ContentBlock>
);
}
function BacktraceCopyDisplay({
backtrace,
setBacktrace,
}: {
backtrace: string;
setBacktrace: (newValue: string) => void;
}) {
return ( return (
<BacktraceWrapper> <BacktraceWrapper>
<label>Backtrace:</label> <BacktraceInputBox
<input type="text" value={backtrace} /> type="text"
<button placeholder="Enter the backtrace code here"
onChange={(e) => setBacktrace(e.target.value)}
value={backtrace}
/>
<BacktraceCopyButton
onClick={() => { onClick={() => {
navigator.clipboard.writeText(backtrace); navigator.clipboard.writeText(backtrace);
}} }}
> >
Copy Copy
</button> </BacktraceCopyButton>
</BacktraceWrapper> </BacktraceWrapper>
); );
} }
const BacktraceInputBox = styled.input`
font-size: larger;
background-color: #eee;
border: 1px solid #aaa;
border-radius: 4px;
min-width: 0;
flex-grow: 999;
`;
const BacktraceWrapper = styled.section`
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
`;
const BacktraceCopyButton = styled.button`
padding: 10px;
`;
function getBacktrace() {
return window.location.hash.slice(1);
}

View file

@ -0,0 +1,210 @@
import { styled } from "styled-components";
import { AddressInfo, AgbDebug, useAgbDebug } from "../useAgbDebug.hook";
import { ReactNode, useMemo, useState } from "react";
const BacktraceListWrapper = styled.div`
font-size: 1rem;
`;
const BacktraceList = styled.ol`
overflow-x: scroll;
white-space: nowrap;
`;
interface DebugProps {
encodedBacktrace: string;
}
export function Debug(props: DebugProps) {
const debug = useAgbDebug();
if (debug) {
return <DebugBacktraceDecode debug={debug} {...props} />;
} else {
return <p>Loading debug viewer...</p>;
}
}
interface DebugBacktraceDecodeProps extends DebugProps {
debug: AgbDebug;
}
const NonWrapCode = styled.code`
white-space: nowrap;
`;
function DebugBacktraceDecode({
encodedBacktrace,
debug,
}: DebugBacktraceDecodeProps) {
const backtraceAddresses = useBacktraceData(debug, encodedBacktrace);
const [backtraceLocations, setBacktraceLocations] = useState<AddressInfo[][]>(
[]
);
const [backtraceLocationsError, setBacktraceLocationsError] =
useState<string>("");
if (typeof backtraceAddresses === "string") {
return (
<DebugError>
Something went wrong decoding the backtrace: {backtraceAddresses}
</DebugError>
);
}
return (
<>
<BacktraceListWrapper>
<BacktraceList>
{backtraceAddresses.map((x, idx) => (
<li key={idx}>
<BacktraceAddressInfo
address={x}
info={backtraceLocations[idx]}
/>
</li>
))}
</BacktraceList>
</BacktraceListWrapper>
<p>
If you add the elf file used to make the GBA file, or the GBA file
itself if it was made with <NonWrapCode>agb-gbafix --debug</NonWrapCode>
, you can see: function names, file names, line numbers, and column
numbers.
</p>
<label>
Elf file or GBA file with debug information:{" "}
<input
type="file"
onChange={(evt) => {
const files = evt.target.files;
if (!files) return;
const file = files[0];
if (!file) return;
setBacktraceLocationsError("");
loadLocations(debug, backtraceAddresses, file)
.then((data) => setBacktraceLocations(data))
.catch((e) => setBacktraceLocationsError(`${e}`));
}}
/>
</label>
{backtraceLocationsError && (
<DebugError>
Something went wrong looking up the addresses in the file provided:{" "}
{backtraceLocationsError}
</DebugError>
)}
</>
);
}
const ErrorBlock = styled.div`
background-color: #f78f8f;
border: 2px solid #9c0a0a;
border-radius: 8px;
padding: 20px;
margin-top: 10px;
`;
function DebugError({ children }: { children: ReactNode }) {
return <ErrorBlock>{children}</ErrorBlock>;
}
function makeNicePath(path: string) {
const srcIndex = path.lastIndexOf("/src/");
if (srcIndex < 0) return path;
const crateNameStartIndex = path.slice(0, srcIndex).lastIndexOf("/");
const crateName =
crateNameStartIndex < 0
? "<crate>"
: path.slice(crateNameStartIndex + 1, srcIndex);
return `<${crateName}>/${path.slice(srcIndex + 5)}`;
}
const GreenSpan = styled.span`
color: green;
`;
const BacktraceAddressLine = styled.ul`
list-style-type: none;
padding-left: 20px;
`;
function BacktraceAddressInfo({
address,
info,
}: {
address: number;
info: AddressInfo[] | undefined;
}) {
const formattedAddress = `0x${address.toString(16).padStart(8, "0")}`;
if (!info) {
return <code>{formattedAddress}</code>;
}
if (info.length === 0) {
return (
<BacktraceAddressLine>
<li>
<code>(no info) {formattedAddress}</code>
</li>
</BacktraceAddressLine>
);
}
function FunctionName({
interesting,
functionName,
}: {
interesting: boolean;
functionName: string;
}) {
if (interesting) {
return <strong>{functionName}</strong>;
}
return functionName;
}
console.log(info);
return (
<BacktraceAddressLine>
{info.map((x, idx) => (
<li key={idx}>
<code>
{x.is_inline && "(inlined into)"}{" "}
<FunctionName
interesting={x.is_interesting}
functionName={x.function_name}
/>{" "}
<GreenSpan>
{makeNicePath(x.filename)}:{x.line_number}:{x.column}
</GreenSpan>
</code>
</li>
))}
</BacktraceAddressLine>
);
}
async function loadLocations(debug: AgbDebug, addresses: number[], file: File) {
const buf = await file.arrayBuffer();
const view = new Uint8Array(buf);
const agbDebugFile = debug.debug_file(view);
const debugInfo = addresses.map((x) => agbDebugFile.address_info(x));
return debugInfo;
}
function useBacktraceData(debug: AgbDebug, trace: string) {
return useMemo(() => {
try {
const addresses = debug?.decode_backtrace(trace);
return addresses && Array.from(addresses);
} catch (e: unknown) {
return `${e}`;
}
}, [debug, trace]);
}

View file

@ -0,0 +1,23 @@
import { styled } from "styled-components";
export function GameDeveloperSummary() {
return (
<Details>
<Summary>For game developers</Summary>
<p>If you don&apos;t want players to be sent to this page, you can:</p>
<ol>
<li>Configure the backtrace page to point to your own site</li>
<li>Configure the backtrace page to not point to a site at all</li>
<li>Not use the backtrace feature</li>
</ol>
</Details>
);
}
const Details = styled.details`
margin-top: 10px;
`;
const Summary = styled.summary`
font-weight: bold;
`;

View file

@ -1,21 +1,10 @@
import { Metadata } from "next"; import { Metadata } from "next";
import { BacktraceDisplay } from "./backtrace"; import { BacktracePage } from "./backtrace";
import { ContentBlock } from "../contentBlock";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "agbrs crash backtrace", title: "agbrs crash backtrace",
}; };
export default function Crash() { export default function Backtrace() {
return ( return <BacktracePage />;
<ContentBlock>
<h1>agbrs crash backtrace viewer</h1>
<p>This page will eventually let you view backtraces in the browser.</p>
<p>
For now you can copy the backtrace code here and use it with{" "}
<code>agb-addr2line</code>
</p>
<BacktraceDisplay />
</ContentBlock>
);
} }

View file

@ -0,0 +1,37 @@
import { useEffect, useState } from "react";
import debugInit, {
decode_backtrace,
DebugFile,
InitOutput,
AddressInfo,
} from "./vendor/backtrace/backtrace";
let agbDebug: Promise<InitOutput> | undefined;
export { AddressInfo };
export interface AgbDebug {
decode_backtrace: (backtrace: string) => Uint32Array;
debug_file: (file: Uint8Array) => DebugFile;
}
export function useAgbDebug(): AgbDebug | undefined {
const [debug, setDebug] = useState<AgbDebug>();
useEffect(() => {
(async () => {
if (agbDebug === undefined) {
agbDebug = debugInit();
}
await agbDebug;
setDebug({
decode_backtrace,
debug_file: (file: Uint8Array) => new DebugFile(file),
});
})();
}, []);
return debug;
}

View file

@ -0,0 +1,29 @@
[package]
name = "backtrace"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
agb-debug = { path="../../agb-debug" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

View file

@ -0,0 +1,49 @@
use agb_debug::{addr2line::Context, load_dwarf, Addr2LineContext};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn decode_backtrace(backtrace: &str) -> Result<Vec<u32>, JsError> {
Ok(agb_debug::gwilym_decode(backtrace)?.collect())
}
#[wasm_bindgen]
pub struct DebugFile {
dwarf: Addr2LineContext,
}
#[wasm_bindgen(getter_with_clone)]
pub struct AddressInfo {
pub filename: String,
pub function_name: String,
pub line_number: u32,
pub column: u32,
pub is_interesting: bool,
pub is_inline: bool,
}
#[wasm_bindgen]
impl DebugFile {
#[wasm_bindgen(constructor)]
pub fn new(data: &[u8]) -> Result<DebugFile, JsError> {
Ok(DebugFile {
dwarf: Context::from_dwarf(load_dwarf(data)?)?,
})
}
pub fn address_info(&self, address: u32) -> Result<Vec<AddressInfo>, JsError> {
let info = agb_debug::address_info(&self.dwarf, address.into())?;
let address_infos = info
.into_iter()
.map(|x| AddressInfo {
filename: x.location.filename,
line_number: x.location.line,
column: x.location.col,
is_interesting: x.is_interesting,
is_inline: x.is_inline,
function_name: x.function,
})
.collect();
Ok(address_infos)
}
}