Files
swift-mirror/stdlib/public/RuntimeModule/BacktraceJSONFormatter.swift
2025-12-12 16:32:02 +00:00

388 lines
11 KiB
Swift

//===--- BacktraceJSONFormatter.swift -------------------------*- swift -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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 JSON backtraces.
//
//===----------------------------------------------------------------------===//
import Swift
@_spi(Formatting)
public struct BacktraceJSONFormatterOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let allRegisters = BacktraceJSONFormatterOptions(rawValue: 1<<0)
public static let demangle = BacktraceJSONFormatterOptions(rawValue: 1<<1)
public static let sanitize = BacktraceJSONFormatterOptions(rawValue: 1<<2)
public static let mentionedImages = BacktraceJSONFormatterOptions(rawValue: 1<<3)
public static let allThreads = BacktraceJSONFormatterOptions(rawValue: 1<<4)
public static let images = BacktraceJSONFormatterOptions(rawValue: 1<<5)
}
internal extension BacktraceJSONFormatterOptions {
// this is not the form we want but will do for now
// we are going to turn this into an OptionSet or similar
var showAllRegisters: Bool { contains(.allRegisters) }
var shouldDemangle: Bool { contains(.demangle) }
var shouldSanitize: Bool { contains(.sanitize) }
var showMentionedImages: Bool { contains(.mentionedImages) }
var showAllThreads: Bool { contains(.allThreads) }
var showImages: Bool { contains(.images) }
}
@_spi(Formatting)
public protocol BacktraceJSONWriter {
func write(_ string: String, flush: Bool)
func writeln(_ string: String, flush: Bool)
}
@_spi(Formatting)
public struct BacktraceJSONFormatter<
Address: FixedWidthInteger,
Writer: BacktraceJSONWriter> {
var writer: Writer
var options: BacktraceJSONFormatterOptions
typealias Log = CrashLog<Address>
var crashLog: Log
var imageMap: ImageMap?
var mentionedImages: Set<Int> = []
@_spi(Formatting)
public init(
crashLog: CrashLog<Address>,
writer: Writer,
options: BacktraceJSONFormatterOptions)
{
self.crashLog = crashLog
self.writer = writer
self.options = options
self.imageMap = crashLog.imageMap()
}
}
internal extension BacktraceJSONFormatter {
func write(_ string: String, flush: Bool) {
writer.write(string, flush: flush)
}
func writeln(_ string: String, flush: Bool) {
writer.writeln(string, flush: flush)
}
func getDescription() -> String? {
crashLog.description
}
func getArchitecture() -> String? {
crashLog.architecture
}
func getPlatform() -> String? {
crashLog.platform
}
func getFaultAddress() -> String? {
crashLog.faultAddress
}
}
@_spi(Formatting)
public extension BacktraceJSONFormatter {
mutating func writeCrashLog(now: String) {
writePreamble(now: now)
writeThreads()
writeCapturedMemory()
writeImages()
writeFooter()
}
}
@_spi(Formatting)
public extension BacktraceJSONFormatter {
func writePreamble(now: String) {
guard let description = getDescription(),
let faultAddress = getFaultAddress(),
let platform = getPlatform(),
let architecture = getArchitecture() else { return }
write("""
{ \
"timestamp": "\(now)", \
"kind": "crashReport", \
"description": "\(escapeJSON(description))", \
"faultAddress": "\(faultAddress)", \
"platform": "\(escapeJSON(platform))", \
"architecture": "\(escapeJSON(architecture))"
""",
flush: false)
}
// this updates the mentionedImages and omittedImages properties
// it's done here rather than in capture for efficiency, as at that
// point we are not sure if we are limiting to mentioned images or not
mutating func writeThreads() {
write(#", "threads": [ "#, flush: false)
let threads = crashLog.threads.filter { options.showAllThreads || $0.crashed }
var first = true
for thread in threads {
if first {
first = false
} else {
write(", ", flush: false)
}
writeThread(thread: thread)
}
write(" ]", flush: false)
if !options.showAllThreads && crashLog.threads.count > 1 {
write(", \"omittedThreads\": \(crashLog.threads.count - 1)", flush: false)
}
// if omittedImages is nil, try to calculate it.
if let images = crashLog.images,
options.showMentionedImages,
self.crashLog.omittedImages == nil {
self.crashLog.omittedImages = images.count - mentionedImages.count
}
}
func writeCapturedMemory() {
// suppress writing captured memory if we should sanitize
if !options.shouldSanitize, let capturedMemory = crashLog.capturedMemory {
write(#", "capturedMemory": {"#, flush: false)
var first = true
for (address, bytes) in capturedMemory.sorted(by: <) {
if first {
first = false
} else {
write(",", flush: false)
}
write(" \"\(address)\": \"\(bytes)\"", flush: false)
}
write(" }", flush: false)
}
}
// note: this updates the crash log with the ommitted image count
func writeImages() {
if options.showImages, let images = crashLog.images {
var imagesToWrite = images
if options.showMentionedImages {
let mentioned =
images.enumerated()
.filter { mentionedImages.contains($0.0) }
.map { $0.1 }
imagesToWrite = mentioned
write(", \"omittedImages\": \(self.crashLog.omittedImages ?? 0)",
flush: false)
}
write(", \"images\": [ ", flush: false)
var first = true
for image in imagesToWrite {
writeImage(image, first: first)
if first {
first = false
}
}
write(" ] ", flush: false)
}
}
func writeFooter() {
write(", \"backtraceTime\": \(crashLog.backtraceTime) ", flush: false)
write("}", flush: false)
}
}
internal extension BacktraceJSONFormatter {
func writeThreadRegisters(thread: Log.Thread) {
guard let registers = thread.registers else { return }
var first = true
let registerOrder = HostContext.registerDumpOrder
for registerName in registerOrder {
if let value = registers[registerName] {
if first {
first = false
} else {
write(", ", flush: false)
}
write("\"\(registerName)\": \"\(value)\"", flush: false)
}
}
}
func imageByAddress(_ address: Address) -> Int? {
guard let address = Backtrace.Address(address) else { return nil }
return imageMap?.indexOfImage(at: address)
}
// note: this updates the mentioned images
mutating func writeThread(
thread: Log.Thread) {
write("{ ", flush: false)
if let name = thread.name, !name.isEmpty {
write("\"name\": \"\(escapeJSON(name))\", ", flush: false)
}
let isCrashingThread = thread.crashed
if isCrashingThread {
write(#""crashed": true, "#, flush: false)
}
if options.showAllRegisters || isCrashingThread {
write(#""registers": {"#, flush: false)
writeThreadRegisters(thread: thread)
write(" }, ", flush: false)
}
write(#""frames": ["#, flush: false)
var first = true
for frame in thread.frames {
if first {
first = false
} else {
write(",", flush: false)
}
write(" { ", flush: false)
write(frame.jsonBody, flush: false)
if frame.inlined {
write(#", "inlined": true"#, flush: false)
}
if frame.isSwiftRuntimeFailure {
write(#", "runtimeFailure": true"#, flush: false)
}
if frame.isSwiftThunk {
write(#", "thunk": true"#, flush: false)
}
if frame.isSystem {
write(#", "system": true"#, flush: false)
}
if let symbol = frame.symbol {
write("""
, "symbol": "\(escapeJSON(symbol))"\
, "offset": \(frame.offset ?? 0)
""", flush: false)
if options.shouldDemangle, let demangledName = frame.demangledName {
let formattedOffset: String
if (frame.offset ?? 0) > 0 {
formattedOffset = " + \(frame.offset!)"
} else if (frame.offset ?? 0) < 0 {
formattedOffset = " - \(frame.offset!)"
} else {
formattedOffset = ""
}
write("""
, "description": \"\(escapeJSON(demangledName))\(formattedOffset)\"
""", flush: false)
}
if let image = frame.image {
write(", \"image\": \"\(image)\"", flush: false)
}
if var sourceLocation = frame.sourceLocation {
if options.shouldSanitize {
sourceLocation.file = sanitizePath(sourceLocation.file)
}
write(#", "sourceLocation": { "#, flush: false)
write("""
"file": "\(escapeJSON(sourceLocation.file))", \
"line": \(sourceLocation.line), \
"column": \(sourceLocation.column)
""", flush: false)
write(" }", flush: false)
}
}
write(" }", flush: false)
}
write(" ] ", flush: false)
write("}", flush: false)
if options.showMentionedImages {
for frame in thread.frames {
if let imageName = frame.image,
let imageIndex = crashLog.images?
.firstIndex(where: { $0.name == imageName }) {
mentionedImages.insert(imageIndex)
} else if let addressString = frame.address,
let address = Log.addressFromString(addressString),
let imageIndex = imageByAddress(address) {
mentionedImages.insert(imageIndex)
}
}
}
}
func writeImage(_ image: Log.Image, first: Bool) {
if !first {
write(", ", flush: false)
}
write("{ ", flush: false)
if let name = image.name {
write("\"name\": \"\(escapeJSON(name))\", ", flush: false)
}
if let buildId = image.buildId {
write("\"buildId\": \"\(buildId)\", ", flush: false)
}
if var path = image.path {
if options.shouldSanitize {
path = sanitizePath(path)
}
write("\"path\": \"\(path)\", ", flush: false)
}
write("""
"baseAddress": "\(image.baseAddress)", \
"endOfText": "\(image.endOfText)"
""", flush: false)
write(" }", flush: false)
}
}