Skip to content
This repository was archived by the owner on Oct 28, 2024. It is now read-only.

Introduce a new 'colored' Backtrace format #64

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ $ swift build -c release -Xswiftc -g

When your app crashes, a stacktrace will be printed to `stderr`.

### Formats

The library comes with two built-in stack trace formats:
- `.full`: default format, prints everything in one long line per stack entry
- `.colored`: lighter format with newlines and colors, easier to read but takes up more vertical space

Use `Backtrace.install(format:)` to specify which format you want.

You also have the option to specify your own format using `.custom(formatter: Formatter, skip: Int)`. The `Formatter` closure is executed for every line of the stack trace and prints the returned string.

## Security

Please see [SECURITY.md](SECURITY.md) for details on the security process.
Expand Down
98 changes: 64 additions & 34 deletions Sources/Backtrace/Backtrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,37 @@ typealias CBacktraceSyminfoCallback = @convention(c) (_ data: UnsafeMutableRawPo

private let state = backtrace_create_state(nil, /* BACKTRACE_SUPPORTS_THREADS */ 1, nil, nil)

private let fullCallback: CBacktraceFullCallback? = {
_, pc, filename, lineno, function in
private var installedFormat: Format = .default

var str = "0x"
str.append(String(pc, radix: 16))
private let callback: CBacktraceFullCallback? = { _, pc, filename, lineno, function in
let formattedPc = "0x\(String(pc, radix: 16))"

let demangledFunction: String?
if let function = function {
str.append(", ")
var fn = String(cString: function)
if fn.hasPrefix("$s") || fn.hasPrefix("$S") {
fn = _stdlib_demangleName(fn)
}
str.append(fn)
demangledFunction = fn
} else {
demangledFunction = nil
}

let file: (fileName: String, line: Int)?
if let filename = filename {
str.append(" at ")
str.append(String(cString: filename))
str.append(":")
str.append(String(lineno))
file = (fileName: String(cString: filename), line: Int(lineno))
} else {
file = nil
}
str.append("\n")

str.withCString { ptr in
_ = withVaList([ptr]) { vaList in
vfprintf(stderr, "%s", vaList)
if let line = installedFormat.formatter(formattedPc, demangledFunction, file) {
line.withCString { ptr in
_ = withVaList([ptr]) { vaList in
vfprintf(stderr, "%s", vaList)
}
}
}

return 0
}

Expand All @@ -63,18 +68,20 @@ private let errorCallback: CBacktraceErrorCallback? = {

private func printBacktrace(signal: CInt) {
_ = fputs("Received signal \(signal). Backtrace:\n", stderr)
backtrace_full(state, /* skip */ 0, fullCallback, errorCallback, nil)
backtrace_full(state, Int32(installedFormat.skip), callback, errorCallback, nil)
fflush(stderr)
}

public enum Backtrace {
/// Install the backtrace handler on default signals: `SIGILL`, `SIGSEGV`, `SIGBUS`, `SIGFPE`.
public static func install() {
Backtrace.install(signals: [SIGILL, SIGSEGV, SIGBUS, SIGFPE])
public static func install(format: Format = .default) {
Backtrace.install(signals: [SIGILL, SIGSEGV, SIGBUS, SIGFPE], format: format)
}

/// Install the backtrace handler when any of `signals` happen.
public static func install(signals: [CInt]) {
public static func install(signals: [CInt], format: Format = .default) {
installedFormat = format

for signal in signals {
self.setupHandler(signal: signal) { signal in
printBacktrace(signal: signal)
Expand All @@ -85,7 +92,7 @@ public enum Backtrace {

@available(*, deprecated, message: "This method will be removed in the next major version.")
public static func print() {
backtrace_full(state, /* skip */ 0, fullCallback, errorCallback, nil)
backtrace_full(state, Int32(installedFormat.skip), callback, errorCallback, nil)
}

private static func setupHandler(signal: Int32, handler: @escaping @convention(c) (CInt) -> Void) {
Expand All @@ -105,10 +112,14 @@ public enum Backtrace {
#if swift(<5.4)
#error("unsupported Swift version")
#else
import Foundation

@_implementationOnly import CRT
@_implementationOnly import WinSDK
#endif

private var installedFormat: Format = .default

public enum Backtrace {
private static var MachineType: DWORD {
#if arch(arm)
Expand All @@ -126,12 +137,14 @@ public enum Backtrace {

/// Signal selection unavailable on Windows. Use ``install()-484jy``.
@available(*, deprecated, message: "signal selection unavailable on Windows")
public static func install(signals: [CInt]) {
Backtrace.install()
public static func install(signals: [CInt], format: Format = .default) {
Backtrace.install(format: format)
}

/// Install the backtrace handler on default signals.
public static func install() {
public static func install(format: Format = .default) {
installedFormat = format

// Install a last-chance vectored exception handler to capture the error
// before the termination and report the stack trace. It is unlikely
// that this will be recovered at this point by a SEH handler.
Expand Down Expand Up @@ -188,9 +201,15 @@ public enum Backtrace {
capacity: 1)

let hThread: HANDLE = GetCurrentThread()
var toSkip = installedFormat.skip
while StackWalk64(Backtrace.MachineType, hProcess, hThread,
&Frame, &cxr, nil, SymFunctionTableAccess64,
SymGetModuleBase64, nil) {
if toSkip > 0 {
toSkip -= 1
continue
}

var qwModuleBase: DWORD64 =
SymGetModuleBase64(hProcess, Frame.AddrPC.Offset)

Expand Down Expand Up @@ -229,29 +248,40 @@ public enum Backtrace {
_ = SymGetLineFromAddr64(hProcess, Frame.AddrPC.Offset,
&Displacement, &Line)

var details: String = ""

#if arch(arm64) || arch(x86_64)
let formattedPc = String(format: "%#016x", Frame.AddrPC.Offset)
#else
let formattedPc = String(format: "%#08x", Frame.AddrPC.Offset)
#endif

let demangledFunction: String?
if !symbol.isEmpty {
// Truncate the module path to the filename. The
// `PathFindFileNameW` call will return the beginning of the
// string if a path separator character is not found.
if let pszModule = module.withCString(encodedAs: UTF16.self,
PathFindFileNameW) {
details.append(", \(String(decodingCString: pszModule, as: UTF16.self))!\(symbol)")
demangledFunction = "\(String(decodingCString: pszModule, as: UTF16.self))!\(symbol)"
} else {
demangledFunction = nil
}
} else {
demangledFunction = nil
}

let file: (fileName: String, line: Int)?
if let szFileName = Line.FileName {
details.append(" at \(String(cString: szFileName)):\(Line.LineNumber)")
file = (fileName: String(cString: szFileName), line: Int(Line.LineNumber))
} else {
file = nil
}

_ = details.withCString { pszDetails in
withVaList([Frame.AddrPC.Offset, pszDetails]) {
#if arch(arm64) || arch(x86_64)
vfprintf(stderr, "%#016x%s\n", $0)
#else
vfprintf(stderr, "%#08x%s\n", $0)
#endif
if let line = installedFormat.formatter(formattedPc, demangledFunction, file) {
_ = line.withCString { pszDetails in
withVaList([pszDetails]) {
vfprintf(stderr, "%s", $0)
}
}
}
}
Expand All @@ -267,10 +297,10 @@ public enum Backtrace {
#else
public enum Backtrace {
/// Install the backtrace handler on default signals. Available on Windows and Linux only.
public static func install() {}
public static func install(format: Format = .default) {}

/// Install the backtrace handler on specific signals. Available on Linux only.
public static func install(signals: [CInt]) {}
public static func install(signals: [CInt], format: Format = .default) {}

@available(*, deprecated, message: "This method will be removed in the next major version.")
public static func print() {}
Expand Down
107 changes: 107 additions & 0 deletions Sources/Backtrace/Format.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftLinuxBacktrace open source project
//
// Copyright (c) 2019-2022 Apple Inc. and the SwiftLinuxBacktrace project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftLinuxBacktrace project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Formatter for one backtrace line.
///
/// - Parameter pc: String formated value for PC register ("0x123456789AB").
/// - Parameter function: full unmangled function name if available.
/// - Parameter fileName: tuple containing `(full path, line)` if available.
/// - Returns: The formatted line to print, or `nil` if it should be skipped.
public typealias Formatter = (_ pc: String, _ function: String?, _ file: (String, Int)?) -> String?

/// Different built-in formats for backtraces.
public enum Format {
/// Displays all information on one line.
/// Contains all backtrace lines, including those from Backtrace itself.
/// Useful for debugging full dumps.
case full

/// Formats the information on multiple lines with colors, without PC register.
/// Top backtrace lines from Backtrace itself are ignored.
/// More readable than ``Format/full`` when shown in a short terminal but
/// takes up more vertical space.
case colored

/// Runs the given formatter to format each line individually.
/// - Parameter formatter: The formatter to run.
/// - Parameter skip: How many backtrace lines to skip at the top.
case custom(formatter: Formatter, skip: Int)

/// Default format.
public static let `default` = Format.full

internal var skip: Int {
switch self {
case .full: return 0
case .colored: return 4 // low enough to be safe on Linux and Windows but still reduce output noise
case .custom(_, let skip): return skip
}
}

internal var formatter: Formatter {
switch self {
case .full: return fullFormatter
case .colored: return coloredFormatter
case .custom(let formatter, _): return formatter
}
}
}

let fullFormatter: Formatter = { (_ pc: String, _ function: String?, _ file: (String, Int)?) -> String? in
var str = pc

if let function = function {
str.append(", ")
str.append(function)
}

if let (fileName, line) = file {
str.append(" at ")
str.append(fileName)
str.append(":")
str.append(String(line))
}

str.append("\n")

return str
}

let coloredFormatter: Formatter = { (_ pc: String, _ function: String?, _ file: (String, Int)?) -> String? in
let red = "\u{001B}[91m"
let reset = "\u{001B}[0m"

var str = ""

if let function = function {
str.append(" at ")
str.append(red)
str.append(function)
str.append(reset)
} else {
str.append(" at <unavailable>")
}

str.append("\n")

if let (fileName, line) = file {
str.append(" ")
str.append(fileName)
str.append(":")
str.append(String(line))
str.append("\n")
}

return str
}
8 changes: 8 additions & 0 deletions Sources/Sample/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import Backtrace
import Darwin
#elseif os(Linux)
import Glibc
#elseif os(Windows)
@_implementationOnly import CRT
@_implementationOnly import WinSDK

let SIGILL: Int32 = 4
let SIGSEGV: Int32 = 11
let SIGBUS: Int32 = 10
let SIGFPE: Int32 = 8
#endif

Backtrace.install()
Expand Down
38 changes: 38 additions & 0 deletions Tests/BacktraceTests/FormatsTests+XCTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftLinuxBacktrace open source project
//
// Copyright (c) 2019-2020 Apple Inc. and the SwiftLinuxBacktrace project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftLinuxBacktrace project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// FormatsTests+XCTest.swift
//
import XCTest

///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///

extension FormatsTests {
public static var allTests: [(String, (FormatsTests) -> () throws -> Void)] {
return [
("testFullFormat", testFullFormat),
("testFullFormatNoFunction", testFullFormatNoFunction),
("testFullFormatNoFile", testFullFormatNoFile),
("testFullFormatNoFileNoFunction", testFullFormatNoFileNoFunction),
("testColoredFormat", testColoredFormat),
("testColoredFormatNoFunction", testColoredFormatNoFunction),
("testColoredFormatNoFile", testColoredFormatNoFile),
("testColoredFormatNoFileNoFunction", testColoredFormatNoFileNoFunction),
]
}
}
Loading