mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
Move the backtracing code into a new Runtime module. This means renaming the Swift Runtime's CMake target because otherwise there will be a name clash. rdar://124913332
1013 lines
32 KiB
Swift
1013 lines
32 KiB
Swift
//===--- BacktraceFormatter.swift -----------------------------*- swift -*-===//
|
||
//
|
||
// This source file is part of the Swift.org open source project
|
||
//
|
||
// Copyright (c) 2023 Apple Inc. and the Swift project authors
|
||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||
//
|
||
// See https://swift.org/LICENSE.txt for license information
|
||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
//
|
||
// Provides functionality to format backtraces, with various additional
|
||
// options.
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
import Swift
|
||
|
||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||
internal import Darwin
|
||
internal import BacktracingImpl.OS.Darwin
|
||
#elseif os(Windows)
|
||
internal import ucrt
|
||
#elseif canImport(Glibc)
|
||
internal import Glibc
|
||
#elseif canImport(Musl)
|
||
internal import Musl
|
||
#endif
|
||
|
||
/// A backtrace formatting theme.
|
||
@_spi(Formatting)
|
||
public protocol BacktraceFormattingTheme {
|
||
func frameIndex(_ s: String) -> String
|
||
func programCounter(_ s: String) -> String
|
||
func frameAttribute(_ s: String) -> String
|
||
func symbol(_ s: String) -> String
|
||
func offset(_ s: String) -> String
|
||
func sourceLocation(_ s: String) -> String
|
||
func lineNumber(_ s: String) -> String
|
||
func code(_ s: String) -> String
|
||
func crashedLineNumber(_ s: String) -> String
|
||
func crashedLine(_ s: String) -> String
|
||
func crashLocation() -> String
|
||
func imageName(_ s: String) -> String
|
||
func imageAddressRange(_ s: String) -> String
|
||
func imageBuildID(_ s: String) -> String
|
||
func imagePath(_ s: String) -> String
|
||
}
|
||
|
||
extension BacktraceFormattingTheme {
|
||
public func frameIndex(_ s: String) -> String { return s }
|
||
public func programCounter(_ s: String) -> String { return s }
|
||
public func frameAttribute(_ s: String) -> String { return "[\(s)]" }
|
||
public func symbol(_ s: String) -> String { return s }
|
||
public func offset(_ s: String) -> String { return s }
|
||
public func sourceLocation(_ s: String) -> String { return s }
|
||
public func lineNumber(_ s: String) -> String { return " \(s)|" }
|
||
public func code(_ s: String) -> String { return s }
|
||
public func crashedLineNumber(_ s: String) -> String { return "*\(s)|" }
|
||
public func crashedLine(_ s: String) -> String { return s }
|
||
public func crashLocation() -> String { return "^" }
|
||
public func imageName(_ s: String) -> String { return s }
|
||
public func imageAddressRange(_ s: String) -> String { return s }
|
||
public func imageBuildID(_ s: String) -> String { return s }
|
||
public func imagePath(_ s: String) -> String { return s}
|
||
}
|
||
|
||
/// Options for backtrace formatting.
|
||
///
|
||
/// This is used by chaining modifiers, e.g. .theme(.color).showSourceCode().
|
||
@_spi(Formatting)
|
||
public struct BacktraceFormattingOptions {
|
||
var _theme: BacktraceFormattingTheme = BacktraceFormatter.Themes.plain
|
||
var _showSourceCode: Bool = false
|
||
var _sourceContextLines: Int = 2
|
||
var _showAddresses: Bool = true
|
||
var _showImages: ImagesToShow = .mentioned
|
||
var _showImageNames: Bool = true
|
||
var _showFrameAttributes: Bool = true
|
||
var _skipRuntimeFailures: Bool = false
|
||
var _skipThunkFunctions: Bool = true
|
||
var _skipSystemFrames: Bool = true
|
||
var _sanitizePaths: Bool = true
|
||
var _demangle: Bool = true
|
||
var _width: Int = 80
|
||
|
||
public var selectedTheme: BacktraceFormattingTheme { return _theme }
|
||
public var shouldShowSourceCode: Bool { return _showSourceCode }
|
||
public var sourceContextLines: Int { return _sourceContextLines }
|
||
public var shouldShowAddresses: Bool { return _showAddresses }
|
||
public var imagesToShow: ImagesToShow { return _showImages }
|
||
public var shouldShowImageNames: Bool { return _showImageNames }
|
||
public var shouldShowFrameAttributes: Bool { return _showFrameAttributes }
|
||
public var shouldSkipRuntimeFailures: Bool { return _skipRuntimeFailures }
|
||
public var shouldSkipThunkFunctions: Bool { return _skipThunkFunctions }
|
||
public var shouldSkipSystemFrames: Bool { return _skipSystemFrames }
|
||
public var shouldSanitizePaths: Bool { return _sanitizePaths }
|
||
public var shouldDemangle: Bool { return _demangle }
|
||
public var formattingWidth: Int { return _width }
|
||
|
||
public init() {}
|
||
|
||
/// Theme to use for formatting.
|
||
///
|
||
/// @param theme A `BacktraceFormattingTheme` structure.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().theme(theme)
|
||
}
|
||
public func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._theme = theme
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable the display of source code in the backtrace.
|
||
///
|
||
/// @param enabled Whether or not to enable source code.
|
||
///
|
||
/// @param contextLines The number of lines of context either side of the
|
||
/// line associated with the backtrace frame.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().showSourceCode(enabled,
|
||
contextLines: contextLines)
|
||
}
|
||
public func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._showSourceCode = enabled
|
||
newOptions._sourceContextLines = contextLines
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable the display of raw addresses.
|
||
///
|
||
/// @param enabled If false, we will only display a raw address in the
|
||
/// backtrace if we haven't been able to symbolicate.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().showAddresses(enabled)
|
||
}
|
||
public func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._showAddresses = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable the display of the image list.
|
||
///
|
||
/// @param enabled Says whether or not to output the image list.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public enum ImagesToShow {
|
||
case none
|
||
case mentioned
|
||
case all
|
||
}
|
||
public static func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().showImages(toShow)
|
||
}
|
||
public func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._showImages = toShow
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable the display of image names in the frame list.
|
||
///
|
||
/// @param enabled If true, we will display the name of the image for
|
||
/// each frame.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().showImageNames(enabled)
|
||
}
|
||
public func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._showImageNames = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable the display of frame attributes in the frame list.
|
||
///
|
||
/// @param enabled If true, we will display the frame attributes.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().showFrameAttributes(enabled)
|
||
}
|
||
public func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._showFrameAttributes = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Set whether or not to show Swift runtime failure frames.
|
||
///
|
||
/// @param enabled If true, we will skip Swift runtime failure frames.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().skipRuntimeFailures(enabled)
|
||
}
|
||
public func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._skipRuntimeFailures = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Set whether or not to show Swift thunk function frames.
|
||
///
|
||
/// @param enabled If true, we will skip Swift thunk function frames.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().skipThunkFunctions(enabled)
|
||
}
|
||
public func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._skipThunkFunctions = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Set whether or not to show system frames.
|
||
///
|
||
/// For instance, on macOS, this will cause us to skip the "start" frame
|
||
/// at the very top of the stack.
|
||
///
|
||
/// @param enabled If true, we will skip system frames.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().skipSystemFrames(enabled)
|
||
}
|
||
public func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._skipSystemFrames = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Enable or disable path sanitization.
|
||
///
|
||
/// This is intended to avoid leaking PII into crash logs.
|
||
///
|
||
/// @param enabled If true, paths will be sanitized.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().sanitizePaths(enabled)
|
||
}
|
||
public func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._sanitizePaths = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Set whether we show mangled or demangled names.
|
||
///
|
||
/// @param enabled If true, we show demangled names if we have them.
|
||
///
|
||
/// @returns A new `BacktraceFormattingOptions` structure.
|
||
public static func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().demangle(enabled)
|
||
}
|
||
public func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._demangle = enabled
|
||
return newOptions
|
||
}
|
||
|
||
/// Set the output width.
|
||
///
|
||
/// @param width The output width in characters. This is only used to
|
||
/// highlight information, and defaults to 80.
|
||
///
|
||
/// returns A new `BacktraceFormattingOptions` structure.
|
||
public static func width(_ width: Int) -> BacktraceFormattingOptions {
|
||
return BacktraceFormattingOptions().width(width)
|
||
}
|
||
public func width(_ width: Int) -> BacktraceFormattingOptions {
|
||
var newOptions = self
|
||
newOptions._width = width
|
||
return newOptions
|
||
}
|
||
}
|
||
|
||
/// Return the width of a given Unicode.Scalar.
|
||
///
|
||
/// It would be nice to have the Unicode width data, which would let us do
|
||
/// a better job of this.
|
||
private func measure(_ ch: Unicode.Scalar) -> Int {
|
||
if ch.isASCII {
|
||
return 1
|
||
}
|
||
|
||
if ch.properties.isEmoji {
|
||
return 2
|
||
}
|
||
|
||
if ch.properties.isIdeographic
|
||
&& !(ch.value >= 0xff61 && ch.value <= 0xffdc)
|
||
&& !(ch.value >= 0xffe8 && ch.value <= 0xffee) {
|
||
return 2
|
||
}
|
||
|
||
if ch.properties.canonicalCombiningClass.rawValue != 0 {
|
||
return 0
|
||
}
|
||
|
||
switch ch.properties.generalCategory {
|
||
case .control, .nonspacingMark:
|
||
return 0
|
||
default:
|
||
return 1
|
||
}
|
||
}
|
||
|
||
/// Compute the width of the given string, ignoring CSI formatting codes.
|
||
private enum MeasureState {
|
||
// Normal state
|
||
case normal
|
||
|
||
// Start of an escape
|
||
case escape
|
||
|
||
// In a CSI escape
|
||
case csi
|
||
}
|
||
|
||
private func measure<S: StringProtocol>(_ s: S) -> Int {
|
||
var totalWidth = 0
|
||
var state: MeasureState = .normal
|
||
|
||
for ch in s.unicodeScalars {
|
||
switch state {
|
||
case .normal:
|
||
if ch.value == 27 {
|
||
// This is an escape sequence
|
||
state = .escape
|
||
} else {
|
||
totalWidth += measure(ch)
|
||
}
|
||
case .escape:
|
||
if ch.value == 0x5b {
|
||
state = .csi
|
||
} else {
|
||
state = .normal
|
||
}
|
||
case .csi:
|
||
if ch.value >= 0x40 && ch.value <= 0x7e {
|
||
state = .normal
|
||
}
|
||
}
|
||
}
|
||
return totalWidth
|
||
}
|
||
|
||
/// Pad the given string to the given width using spaces.
|
||
private func pad(_ s: String, to width: Int,
|
||
aligned alignment: BacktraceFormatter.Alignment = .left)
|
||
-> String {
|
||
|
||
let currentWidth = measure(s)
|
||
let padding = max(width - currentWidth, 0)
|
||
|
||
switch alignment {
|
||
case .left:
|
||
let spaces = String(repeating: " ", count: padding)
|
||
|
||
return s + spaces
|
||
case .right:
|
||
let spaces = String(repeating: " ", count: padding)
|
||
|
||
return spaces + s
|
||
case .center:
|
||
let left = padding / 2
|
||
let right = padding - left
|
||
let leftSpaces = String(repeating: " ", count: left)
|
||
let rightSpaces = String(repeating: " ", count: right)
|
||
|
||
return "\(leftSpaces)\(s)\(rightSpaces)"
|
||
}
|
||
}
|
||
|
||
/// Untabify the given string, assuming tabs of the specified size.
|
||
///
|
||
/// @param s The string to untabify.
|
||
/// @param tabWidth The tab width to assume (default 8).
|
||
///
|
||
/// @returns A string with all the tabs replaced with appropriate numbers
|
||
/// of spaces.
|
||
private func untabify(_ s: String, tabWidth: Int = 8) -> String {
|
||
var result: String = ""
|
||
var first = true
|
||
for chunk in s.split(separator: "\t", omittingEmptySubsequences: false) {
|
||
if first {
|
||
first = false
|
||
} else {
|
||
let toTabStop = tabWidth - measure(result) % tabWidth
|
||
result += String(repeating: " ", count: toTabStop)
|
||
}
|
||
result += chunk
|
||
}
|
||
return result
|
||
}
|
||
|
||
/// Sanitize a path to remove usernames, volume names and so on.
|
||
///
|
||
/// The point of this function is to try to remove anything that might
|
||
/// contain PII before it ends up in a log file somewhere.
|
||
///
|
||
/// @param path The path to sanitize.
|
||
///
|
||
/// @returns A string containing the sanitized path.
|
||
private func sanitizePath(_ path: String) -> String {
|
||
#if os(macOS)
|
||
return CRCopySanitizedPath(path,
|
||
kCRSanitizePathGlobAllTypes
|
||
| kCRSanitizePathKeepFile)
|
||
#else
|
||
// For now, on non-macOS systems, do nothing
|
||
return path
|
||
#endif
|
||
}
|
||
|
||
/// Trim whitespace from the right hand end of a string.
|
||
///
|
||
/// @param s The string to trim.
|
||
///
|
||
/// @returns A string with the whitespace trimmed.
|
||
private func rtrim<S: StringProtocol>(_ s: S) -> S.SubSequence {
|
||
if let lastNonWhitespace = s.lastIndex(where: { !$0.isWhitespace }) {
|
||
return s.prefix(through: lastNonWhitespace)
|
||
}
|
||
return s.dropLast(0)
|
||
}
|
||
|
||
/// Responsible for formatting backtraces.
|
||
@_spi(Formatting)
|
||
public struct BacktraceFormatter {
|
||
|
||
/// The formatting options to apply when formatting data.
|
||
public var options: BacktraceFormattingOptions
|
||
|
||
public struct Themes {
|
||
/// A plain formatting theme.
|
||
public struct PlainTheme: BacktraceFormattingTheme {
|
||
}
|
||
|
||
public static let plain = PlainTheme()
|
||
}
|
||
|
||
public init(_ options: BacktraceFormattingOptions) {
|
||
self.options = options
|
||
}
|
||
|
||
public enum TableRow {
|
||
case columns([String])
|
||
case raw(String)
|
||
}
|
||
|
||
public enum Alignment {
|
||
case left
|
||
case right
|
||
case center
|
||
}
|
||
|
||
/// Output a table with each column nicely aligned.
|
||
///
|
||
/// @param rows An array of table rows, each of which holds an array
|
||
/// of table columns.
|
||
///
|
||
/// @result A `String` containing the formatted table.
|
||
public static func formatTable(_ rows: [TableRow],
|
||
alignments: [Alignment] = []) -> String {
|
||
// Work out how many columns we have
|
||
let colCount = rows.map{
|
||
if case let .columns(columns) = $0 {
|
||
return columns.count
|
||
} else {
|
||
return 0
|
||
}
|
||
}.reduce(0, max)
|
||
|
||
// Now compute their widths
|
||
var widths = Array(repeating: 0, count: colCount)
|
||
for row in rows {
|
||
if case let .columns(columns) = row {
|
||
for (n, width) in columns.lazy.map(measure).enumerated() {
|
||
widths[n] = max(widths[n], width)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Generate lines for the table
|
||
var lines: [Substring] = []
|
||
for row in rows {
|
||
switch row {
|
||
case let .columns(columns):
|
||
let line = columns.enumerated().map{ n, column in
|
||
let alignment = n < alignments.count ? alignments[n] : .left
|
||
if n == colCount - 1 && alignment == .left {
|
||
return column
|
||
} else {
|
||
return pad(column, to: widths[n], aligned: alignment)
|
||
}
|
||
}.joined(separator: " ")
|
||
|
||
lines.append(rtrim(line))
|
||
case let .raw(line):
|
||
lines.append(rtrim(line))
|
||
}
|
||
}
|
||
|
||
// Trim any empty lines from the end
|
||
guard let lastNonEmpty = lines.lastIndex(where: { !$0.isEmpty }) else {
|
||
return ""
|
||
}
|
||
|
||
return lines.prefix(through: lastNonEmpty).joined(separator: "\n")
|
||
}
|
||
|
||
/// Format an individual frame into a list of columns.
|
||
///
|
||
/// @param frame The frame to format.
|
||
/// @param index The frame index, if required.
|
||
///
|
||
/// @result An array of strings, one per column.
|
||
public func formatColumns(frame: Backtrace.Frame,
|
||
index: Int? = nil) -> [String] {
|
||
let pc: String
|
||
var attrs: [String] = []
|
||
|
||
switch frame {
|
||
case let .programCounter(address):
|
||
pc = "\(address)"
|
||
case let .returnAddress(address):
|
||
pc = "\(address)"
|
||
attrs.append("ra")
|
||
case let .asyncResumePoint(address):
|
||
pc = "\(address)"
|
||
attrs.append("async")
|
||
case .omittedFrames(_), .truncated:
|
||
pc = "..."
|
||
}
|
||
|
||
var columns: [String] = []
|
||
if let index = index {
|
||
columns.append(options._theme.frameIndex("\(index)"))
|
||
}
|
||
if options._showFrameAttributes {
|
||
columns.append(attrs.map(
|
||
options._theme.frameAttribute
|
||
).joined(separator: " "))
|
||
}
|
||
columns.append(options._theme.programCounter(pc))
|
||
|
||
return columns
|
||
}
|
||
|
||
/// Format a frame into a list of rows.
|
||
///
|
||
/// @param frame The frame to format.
|
||
/// @param index The frame index, if required.
|
||
///
|
||
/// @result An array of table rows.
|
||
public func formatRows(frame: Backtrace.Frame,
|
||
index: Int? = nil) -> [TableRow] {
|
||
return [.columns(formatColumns(frame: frame,
|
||
index: index))]
|
||
}
|
||
|
||
/// Format just one frame.
|
||
///
|
||
/// @param frame The frame to format.
|
||
/// @param index The frame index, if required.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(frame: Backtrace.Frame,
|
||
index: Int? = nil) -> String {
|
||
let rows = formatRows(frame: frame,
|
||
index: index)
|
||
return BacktraceFormatter.formatTable(rows, alignments: [.right])
|
||
}
|
||
|
||
/// Format the frame list from a backtrace.
|
||
///
|
||
/// @param frames The frames to format.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(frames: some Sequence<Backtrace.Frame>) -> String {
|
||
var rows: [TableRow] = []
|
||
|
||
var n = 0
|
||
for frame in frames {
|
||
rows += formatRows(frame: frame, index: n)
|
||
|
||
if case let .omittedFrames(count) = frame {
|
||
n += count
|
||
} else {
|
||
n += 1
|
||
}
|
||
}
|
||
|
||
return BacktraceFormatter.formatTable(rows, alignments: [.right])
|
||
}
|
||
|
||
/// Format a `Backtrace`
|
||
///
|
||
/// @param backtrace The `Backtrace` object to format.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(backtrace: Backtrace) -> String {
|
||
return format(frames: backtrace.frames)
|
||
}
|
||
|
||
/// Grab source lines for a symbolicated backtrace.
|
||
///
|
||
/// Tries to open the file corresponding to the symbol; if successful,
|
||
/// it will return a string containing the specified lines of context,
|
||
/// with the point at which the program crashed highlighted.
|
||
private func formattedSourceLines(from sourceLocation: SymbolicatedBacktrace.SourceLocation,
|
||
indent theIndent: Int = 2) -> String? {
|
||
guard let fp = fopen(sourceLocation.path, "rt") else {
|
||
return nil
|
||
}
|
||
defer {
|
||
fclose(fp)
|
||
}
|
||
|
||
let indent = String(repeating: " ", count: theIndent)
|
||
var lines: [String] = []
|
||
var line = 1
|
||
let buffer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: 4096)
|
||
var currentLine = ""
|
||
|
||
let maxLine = sourceLocation.line + options._sourceContextLines
|
||
let maxLineWidth = max("\(maxLine)".count, 4)
|
||
|
||
let doLine = { sourceLine in
|
||
if line >= sourceLocation.line - options._sourceContextLines
|
||
&& line <= sourceLocation.line + options._sourceContextLines {
|
||
let untabified = untabify(sourceLine)
|
||
let code = options._theme.code(untabified)
|
||
let theLine: String
|
||
if line == sourceLocation.line {
|
||
let lineNumber = options._theme.crashedLineNumber(pad("\(line)",
|
||
to: maxLineWidth,
|
||
aligned: .right))
|
||
let highlightWidth = options._width - 2 * theIndent
|
||
theLine = options._theme.crashedLine(pad("\(lineNumber) \(code)",
|
||
to: highlightWidth))
|
||
} else {
|
||
let lineNumber = options._theme.lineNumber(pad("\(line)",
|
||
to: maxLineWidth,
|
||
aligned: .right))
|
||
theLine = "\(lineNumber) \(code)"
|
||
}
|
||
lines.append("\(indent)\(theLine)")
|
||
|
||
if line == sourceLocation.line {
|
||
// sourceLocation.column is an index in UTF-8 code units in
|
||
// `untabified`. We should point at the grapheme cluster that
|
||
// contains that UTF-8 index.
|
||
let adjustedColumn: Int
|
||
if sourceLocation.column > 0 {
|
||
adjustedColumn = sourceLocation.column
|
||
} else {
|
||
if let ndx = code.firstIndex(where: { $0 != " " }) {
|
||
adjustedColumn = code.distance(from: code.startIndex, to: ndx) + 1
|
||
} else {
|
||
adjustedColumn = 1
|
||
}
|
||
}
|
||
let utf8Ndx
|
||
= untabified.utf8.index(untabified.utf8.startIndex,
|
||
offsetBy: adjustedColumn,
|
||
limitedBy: untabified.utf8.endIndex)
|
||
?? untabified.utf8.endIndex
|
||
|
||
// Adjust it to point at a grapheme cluster start
|
||
let strNdx = untabified.index(
|
||
untabified.index(utf8Ndx, offsetBy: 1,
|
||
limitedBy: untabified.endIndex)
|
||
?? untabified.endIndex,
|
||
offsetBy: -1,
|
||
limitedBy: untabified.startIndex) ?? untabified.startIndex
|
||
|
||
// Work out the terminal width up to that point
|
||
let terminalWidth = measure(untabified.prefix(upTo: strNdx))
|
||
|
||
let pad = String(repeating: " ",
|
||
count: max(terminalWidth - 1, 0))
|
||
|
||
let marker = options._theme.crashLocation()
|
||
let blankForNumber = options._theme.lineNumber(
|
||
String(repeating: " ", count: maxLineWidth))
|
||
|
||
lines.append("\(indent)\(blankForNumber) \(pad)\(marker)")
|
||
}
|
||
}
|
||
}
|
||
|
||
while feof(fp) == 0 && ferror(fp) == 0 {
|
||
guard let result = fgets(buffer.baseAddress,
|
||
CInt(buffer.count), fp) else {
|
||
break
|
||
}
|
||
|
||
let chunk = String(cString: result)
|
||
currentLine += chunk
|
||
if currentLine.hasSuffix("\n") {
|
||
currentLine.removeLast()
|
||
doLine(currentLine)
|
||
currentLine = ""
|
||
line += 1
|
||
}
|
||
}
|
||
|
||
doLine(currentLine)
|
||
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
/// Format an individual frame into a list of columns.
|
||
///
|
||
/// @params frame The frame to format.
|
||
///
|
||
/// @result An array of strings, one per column.
|
||
public func formatColumns(frame: SymbolicatedBacktrace.Frame,
|
||
index: Int? = nil) -> [String] {
|
||
let pc: String
|
||
var attrs: [String] = []
|
||
|
||
switch frame.captured {
|
||
case let .programCounter(address):
|
||
pc = "\(address)"
|
||
case let .returnAddress(address):
|
||
pc = "\(address)"
|
||
attrs.append("ra")
|
||
case let .asyncResumePoint(address):
|
||
pc = "\(address)"
|
||
attrs.append("async")
|
||
case .omittedFrames(_), .truncated:
|
||
pc = ""
|
||
}
|
||
|
||
if frame.inlined {
|
||
attrs.append("inlined")
|
||
}
|
||
|
||
if frame.isSwiftThunk {
|
||
attrs.append("thunk")
|
||
}
|
||
|
||
if frame.isSystem {
|
||
attrs.append("system")
|
||
}
|
||
|
||
var formattedSymbol: String? = nil
|
||
var hasSourceLocation = false
|
||
|
||
if let symbol = frame.symbol {
|
||
let displayName = options._demangle ? symbol.name : symbol.rawName
|
||
let themedName = options._theme.symbol(displayName)
|
||
|
||
let offset: String
|
||
if symbol.offset > 0 {
|
||
offset = options._theme.offset(" + \(symbol.offset)")
|
||
} else if symbol.offset < 0 {
|
||
offset = options._theme.offset(" - \(-symbol.offset)")
|
||
} else {
|
||
offset = ""
|
||
}
|
||
|
||
let imageName: String
|
||
if options._showImageNames {
|
||
if symbol.imageIndex >= 0 {
|
||
imageName = " in " + options._theme.imageName(symbol.imageName)
|
||
} else {
|
||
imageName = ""
|
||
}
|
||
} else {
|
||
imageName = ""
|
||
}
|
||
|
||
let location: String
|
||
if var sourceLocation = symbol.sourceLocation {
|
||
if options._sanitizePaths {
|
||
sourceLocation.path = sanitizePath(sourceLocation.path)
|
||
}
|
||
location = " at " + options._theme.sourceLocation("\(sourceLocation)")
|
||
hasSourceLocation = true
|
||
} else {
|
||
location = ""
|
||
}
|
||
|
||
formattedSymbol = "\(themedName)\(offset)\(imageName)\(location)"
|
||
}
|
||
|
||
let location: String
|
||
if !hasSourceLocation || options._showAddresses {
|
||
let formattedPc = options._theme.programCounter(pc)
|
||
if let formattedSymbol = formattedSymbol {
|
||
location = "\(formattedPc) \(formattedSymbol)"
|
||
} else {
|
||
location = formattedPc
|
||
}
|
||
} else if let formattedSymbol = formattedSymbol {
|
||
location = formattedSymbol
|
||
} else {
|
||
location = options._theme.programCounter(pc)
|
||
}
|
||
|
||
var columns: [String] = []
|
||
|
||
if let index = index {
|
||
let frameIndex: String
|
||
switch frame.captured {
|
||
case .omittedFrames(_), .truncated:
|
||
frameIndex = options._theme.frameIndex("...")
|
||
default:
|
||
frameIndex = options._theme.frameIndex("\(index)")
|
||
}
|
||
columns.append(frameIndex)
|
||
}
|
||
|
||
if options._showFrameAttributes {
|
||
columns.append(attrs.map(
|
||
options._theme.frameAttribute
|
||
).joined(separator: " "))
|
||
}
|
||
|
||
columns.append(location)
|
||
|
||
return columns
|
||
}
|
||
|
||
/// Format a frame into a list of rows.
|
||
///
|
||
/// @param frame The frame to format.
|
||
/// @param index The frame index, if required.
|
||
///
|
||
/// @result An array of table rows.
|
||
public func formatRows(frame: SymbolicatedBacktrace.Frame,
|
||
index: Int? = nil,
|
||
showSource: Bool = true) -> [TableRow] {
|
||
let columns = formatColumns(frame: frame,
|
||
index: index)
|
||
var rows: [TableRow] = [.columns(columns)]
|
||
|
||
if showSource {
|
||
if let symbol = frame.symbol,
|
||
let sourceLocation = symbol.sourceLocation,
|
||
let lines = formattedSourceLines(from: sourceLocation) {
|
||
rows.append(.raw(""))
|
||
rows.append(.raw(lines))
|
||
rows.append(.raw(""))
|
||
}
|
||
}
|
||
|
||
return rows
|
||
}
|
||
|
||
/// Format just one frame.
|
||
///
|
||
/// @param frame The frame to format.
|
||
/// @param index The frame index, if required.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(frame: SymbolicatedBacktrace.Frame,
|
||
index: Int? = nil,
|
||
showSource: Bool = true) -> String {
|
||
let rows = formatRows(frame: frame, index: index, showSource: showSource)
|
||
return BacktraceFormatter.formatTable(rows, alignments: [.right])
|
||
}
|
||
|
||
/// Return `true` if we should skip the specified frame
|
||
public func shouldSkip(_ frame: SymbolicatedBacktrace.Frame) -> Bool {
|
||
return (options._skipRuntimeFailures && frame.isSwiftRuntimeFailure)
|
||
|| (options._skipSystemFrames && frame.isSystem)
|
||
|| (options._skipThunkFunctions && frame.isSwiftThunk)
|
||
}
|
||
|
||
/// Format the frame list from a symbolicated backtrace.
|
||
///
|
||
/// @param frames The frames to format.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(
|
||
frames: some Sequence<SymbolicatedBacktrace.Frame>
|
||
) -> String {
|
||
var rows: [TableRow] = []
|
||
var sourceLocationsShown = Set<SymbolicatedBacktrace.SourceLocation>()
|
||
|
||
var n = 0
|
||
for frame in frames {
|
||
if shouldSkip(frame) {
|
||
continue
|
||
}
|
||
|
||
var showSource = options._showSourceCode
|
||
if let symbol = frame.symbol,
|
||
let sourceLocation = symbol.sourceLocation {
|
||
if sourceLocationsShown.contains(sourceLocation) {
|
||
showSource = false
|
||
} else {
|
||
sourceLocationsShown.insert(sourceLocation)
|
||
}
|
||
}
|
||
|
||
rows += formatRows(frame: frame, index: n, showSource: showSource)
|
||
|
||
if case let .omittedFrames(count) = frame.captured {
|
||
n += count
|
||
} else {
|
||
n += 1
|
||
}
|
||
}
|
||
|
||
return BacktraceFormatter.formatTable(rows, alignments: [.right])
|
||
}
|
||
|
||
/// Format a `SymbolicatedBacktrace`
|
||
///
|
||
/// @param backtrace The `SymbolicatedBacktrace` object to format.
|
||
///
|
||
/// @result A `String` containing the formatted data.
|
||
public func format(backtrace: SymbolicatedBacktrace) -> String {
|
||
var result = format(frames: backtrace.frames)
|
||
|
||
switch options._showImages {
|
||
case .none:
|
||
break
|
||
case .all:
|
||
result += "\n\nImages:\n"
|
||
result += format(images: backtrace.images)
|
||
case .mentioned:
|
||
var mentionedImages = Set<Int>()
|
||
for frame in backtrace.frames {
|
||
if shouldSkip(frame) {
|
||
continue
|
||
}
|
||
if let symbol = frame.symbol, symbol.imageIndex >= 0 {
|
||
mentionedImages.insert(symbol.imageIndex)
|
||
}
|
||
}
|
||
|
||
let images = mentionedImages.sorted().map{ backtrace.images[$0] }
|
||
let omitted = backtrace.images.count - images.count
|
||
if omitted > 0 {
|
||
result += "\n\nImages (\(omitted) omitted):\n"
|
||
} else {
|
||
result += "\n\nImages (only mentioned):\n"
|
||
}
|
||
result += format(images: images)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
/// Format a `Backtrace.Image` into a list of columns.
|
||
///
|
||
/// @param image The `Image` object to format.
|
||
///
|
||
/// @result An array of strings, one per column.
|
||
public func formatColumns(image: Backtrace.Image) -> [String] {
|
||
let addressRange = "\(image.baseAddress)–\(image.endOfText)"
|
||
let buildID: String
|
||
if let bytes = image.uniqueID {
|
||
buildID = hex(bytes)
|
||
} else {
|
||
buildID = "<no build ID>"
|
||
}
|
||
let imagePath: String
|
||
if let path = image.path {
|
||
if options._sanitizePaths {
|
||
imagePath = sanitizePath(path)
|
||
} else {
|
||
imagePath = path
|
||
}
|
||
} else {
|
||
imagePath = "<unknown>"
|
||
}
|
||
let imageName = image.name ?? "<unknown>"
|
||
return [
|
||
options._theme.imageAddressRange(addressRange),
|
||
options._theme.imageBuildID(buildID),
|
||
options._theme.imageName(imageName),
|
||
options._theme.imagePath(imagePath)
|
||
]
|
||
}
|
||
|
||
/// Format an array of `Backtrace.Image`s.
|
||
///
|
||
/// @param images The array of `Image` objects to format.
|
||
///
|
||
/// @result A string containing the formatted data.
|
||
public func format(images: some Sequence<Backtrace.Image>) -> String {
|
||
let rows = images.map{
|
||
TableRow.columns(
|
||
formatColumns(image: $0)
|
||
)
|
||
}
|
||
|
||
return BacktraceFormatter.formatTable(rows)
|
||
}
|
||
}
|