mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
[Backtracing] Added JSON crash log option.
Also made it so the `sanitize` option causes the crash logs to not include memory dumps. Fixes #71057 rdar://121430255
This commit is contained in:
@@ -194,6 +194,30 @@ public struct Backtrace: CustomStringConvertible, Sendable {
|
||||
return "..."
|
||||
}
|
||||
}
|
||||
|
||||
/// A JSON description of this frame, less the surrounding braces.
|
||||
/// This is useful if you want to add extra information.
|
||||
@_spi(Internal)
|
||||
public var jsonBody: String {
|
||||
switch self {
|
||||
case let .programCounter(addr):
|
||||
return "\"kind\": \"programCounter\", \"address\": \"\(addr)\""
|
||||
case let .returnAddress(addr):
|
||||
return "\"kind\": \"returnAddress\", \"address\": \"\(addr)\""
|
||||
case let .asyncResumePoint(addr):
|
||||
return "\"kind\": \"asyncResumePoint\", \"address\": \"\(addr)\""
|
||||
case let .omittedFrames(count):
|
||||
return "\"kind\": \"omittedFrames\", \"count\": \(count)"
|
||||
case .truncated:
|
||||
return "\"kind\": \"truncated\""
|
||||
}
|
||||
}
|
||||
|
||||
/// A JSON description of this frame.
|
||||
@_spi(Internal)
|
||||
public var jsonDescription: String {
|
||||
return "{ \(jsonBody) }"
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an image loaded in the process's address space
|
||||
|
||||
@@ -416,7 +416,8 @@ private func untabify(_ s: String, tabWidth: Int = 8) -> String {
|
||||
/// @param path The path to sanitize.
|
||||
///
|
||||
/// @returns A string containing the sanitized path.
|
||||
private func sanitizePath(_ path: String) -> String {
|
||||
@_spi(Formatting)
|
||||
public func sanitizePath(_ path: String) -> String {
|
||||
#if os(macOS)
|
||||
return CRCopySanitizedPath(path,
|
||||
kCRSanitizePathGlobAllTypes
|
||||
|
||||
@@ -33,6 +33,7 @@ set(BACKTRACING_COMPILE_FLAGS
|
||||
set(BACKTRACING_SOURCES
|
||||
main.swift
|
||||
AnsiColor.swift
|
||||
JSON.swift
|
||||
TargetMacOS.swift
|
||||
TargetLinux.swift
|
||||
Themes.swift
|
||||
|
||||
362
stdlib/public/libexec/swift-backtrace/JSON.swift
Normal file
362
stdlib/public/libexec/swift-backtrace/JSON.swift
Normal file
@@ -0,0 +1,362 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#elseif canImport(CRT)
|
||||
import CRT
|
||||
#endif
|
||||
|
||||
@_spi(Contexts) import Runtime
|
||||
@_spi(Registers) import Runtime
|
||||
@_spi(Formatting) import Runtime
|
||||
@_spi(Internal) import Runtime
|
||||
@_spi(MemoryReaders) import Runtime
|
||||
|
||||
// ###TODO: Escape JSON strings
|
||||
|
||||
extension SwiftBacktrace {
|
||||
|
||||
static func outputJSONCrashLog() {
|
||||
guard let target = target else {
|
||||
print("swift-backtrace: unable to get target",
|
||||
to: &standardError)
|
||||
return
|
||||
}
|
||||
|
||||
let crashingThread = target.threads[target.crashingThreadNdx]
|
||||
|
||||
let description: String
|
||||
|
||||
if case let .symbolicated(symbolicated) = crashingThread.backtrace,
|
||||
let failure = symbolicated.swiftRuntimeFailure {
|
||||
description = failure
|
||||
} else {
|
||||
description = target.signalDescription
|
||||
}
|
||||
|
||||
write("""
|
||||
{ \
|
||||
"timestamp": "\(formatISO8601(now))", \
|
||||
"kind": "crashReport", \
|
||||
"description": "\(escapeJSON(description))", \
|
||||
"faultAddress": "\(hex(target.faultAddress))"
|
||||
""")
|
||||
|
||||
var mentionedImages = Set<Int>()
|
||||
var capturedBytes: [UInt64:Array<UInt8>] = [:]
|
||||
|
||||
func outputJSONRegister<T: FixedWidthInteger>(
|
||||
name: String, value: T, first: Bool = false
|
||||
) {
|
||||
if !first {
|
||||
write(", ")
|
||||
}
|
||||
write("\"\(name)\": \"\(hex(value))\"")
|
||||
|
||||
if let bytes = try? target.reader.fetch(
|
||||
from: RemoteMemoryReader.Address(value),
|
||||
count: 16,
|
||||
as: UInt8.self) {
|
||||
capturedBytes[UInt64(truncatingIfNeeded: value)] = bytes
|
||||
}
|
||||
}
|
||||
|
||||
func outputJSONRegister<C: Context>(
|
||||
name: String, context: C, register: C.Register, first: Bool = false
|
||||
) {
|
||||
let value = context.getRegister(register)!
|
||||
outputJSONRegister(name: name, value: value, first: first)
|
||||
}
|
||||
|
||||
func outputJSONGPRs<C: Context, Rs: Sequence>(_ context: C, range: Rs)
|
||||
where Rs.Element == C.Register
|
||||
{
|
||||
var first = true
|
||||
for reg in range {
|
||||
outputJSONRegister(name: "\(reg)", context: context, register: reg,
|
||||
first: first)
|
||||
if first {
|
||||
first = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func outputJSONRegisterDump(_ context: X86_64Context) {
|
||||
outputJSONGPRs(context, range: .rax ... .r15)
|
||||
outputJSONRegister(name: "rip", value: context.programCounter)
|
||||
outputJSONRegister(name: "rflags", context: context, register: .rflags)
|
||||
outputJSONRegister(name: "cs", context: context, register: .cs)
|
||||
outputJSONRegister(name: "fs", context: context, register: .fs)
|
||||
outputJSONRegister(name: "gs", context: context, register: .gs)
|
||||
}
|
||||
|
||||
func outputJSONRegisterDump(_ context: I386Context) {
|
||||
outputJSONGPRs(context, range: .eax ... .edi)
|
||||
outputJSONRegister(name: "eip", value: context.programCounter)
|
||||
outputJSONRegister(name: "eflags", context: context, register: .eflags)
|
||||
outputJSONRegister(name: "es", context: context, register: .es)
|
||||
outputJSONRegister(name: "cs", context: context, register: .cs)
|
||||
outputJSONRegister(name: "ss", context: context, register: .ss)
|
||||
outputJSONRegister(name: "ds", context: context, register: .ds)
|
||||
outputJSONRegister(name: "fs", context: context, register: .fs)
|
||||
outputJSONRegister(name: "gs", context: context, register: .gs)
|
||||
}
|
||||
|
||||
func outputJSONRegisterDump(_ context: ARM64Context) {
|
||||
outputJSONGPRs(context, range: .x0 ..< .x29)
|
||||
outputJSONRegister(name: "fp", context: context, register: .x29)
|
||||
outputJSONRegister(name: "lr", context: context, register: .x30)
|
||||
outputJSONRegister(name: "sp", context: context, register: .sp)
|
||||
outputJSONRegister(name: "pc", context: context, register: .pc)
|
||||
}
|
||||
|
||||
func outputJSONRegisterDump(_ context: ARMContext) {
|
||||
outputJSONGPRs(context, range: .r0 ... .r10)
|
||||
outputJSONRegister(name: "fp", context: context, register: .r11)
|
||||
outputJSONRegister(name: "ip", context: context, register: .r12)
|
||||
outputJSONRegister(name: "sp", context: context, register: .r13)
|
||||
outputJSONRegister(name: "lr", context: context, register: .r14)
|
||||
outputJSONRegister(name: "pc", context: context, register: .r15)
|
||||
}
|
||||
|
||||
func outputJSONThread(ndx: Int, thread: TargetThread) {
|
||||
write("{ ")
|
||||
|
||||
if !thread.name.isEmpty {
|
||||
write("\"name\": \"\(escapeJSON(thread.name))\", ")
|
||||
}
|
||||
if thread.id == target.crashingThread {
|
||||
write(#""crashed": true, "#)
|
||||
}
|
||||
if args.registers! == .all {
|
||||
if let context = thread.context {
|
||||
write(#""registers": {"#)
|
||||
outputJSONRegisterDump(context)
|
||||
write(" }, ")
|
||||
}
|
||||
}
|
||||
|
||||
write(#""frames": ["#)
|
||||
var first = true
|
||||
switch thread.backtrace {
|
||||
case let .raw(backtrace):
|
||||
for frame in backtrace.frames {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
write(",")
|
||||
}
|
||||
|
||||
write(" { \(frame.jsonBody) }")
|
||||
}
|
||||
case let .symbolicated(backtrace):
|
||||
for frame in backtrace.frames {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
write(",")
|
||||
}
|
||||
|
||||
write(" { ")
|
||||
|
||||
write(frame.captured.jsonBody)
|
||||
|
||||
if frame.inlined {
|
||||
write(#", "inlined": true"#)
|
||||
}
|
||||
if frame.isSwiftRuntimeFailure {
|
||||
write(#", "runtimeFailure": true"#)
|
||||
}
|
||||
if frame.isSwiftThunk {
|
||||
write(#", "thunk": true"#)
|
||||
}
|
||||
if frame.isSystem {
|
||||
write(#", "system": true"#)
|
||||
}
|
||||
|
||||
if let symbol = frame.symbol {
|
||||
write("""
|
||||
, "symbol": "\(escapeJSON(symbol.rawName))"\
|
||||
, "offset": \(symbol.offset)
|
||||
""")
|
||||
|
||||
if args.demangle {
|
||||
let formattedOffset: String
|
||||
if symbol.offset > 0 {
|
||||
formattedOffset = " + \(symbol.offset)"
|
||||
} else if symbol.offset < 0 {
|
||||
formattedOffset = " - \(symbol.offset)"
|
||||
} else {
|
||||
formattedOffset = ""
|
||||
}
|
||||
|
||||
write("""
|
||||
, "description": \"\(escapeJSON(symbol.name))\(formattedOffset)\"
|
||||
""")
|
||||
}
|
||||
|
||||
if symbol.imageIndex >= 0 {
|
||||
write(", \"image\": \"\(symbol.imageName)\"")
|
||||
}
|
||||
|
||||
if var sourceLocation = symbol.sourceLocation {
|
||||
if args.sanitize ?? false {
|
||||
sourceLocation.path = sanitizePath(sourceLocation.path)
|
||||
}
|
||||
write(#", "sourceLocation": { "#)
|
||||
|
||||
write("""
|
||||
"file": "\(escapeJSON(sourceLocation.path))", \
|
||||
"line": \(sourceLocation.line), \
|
||||
"column": \(sourceLocation.column)
|
||||
""")
|
||||
|
||||
write(" } ")
|
||||
}
|
||||
}
|
||||
write(" }")
|
||||
}
|
||||
}
|
||||
write(" ] ")
|
||||
|
||||
write("} ")
|
||||
|
||||
if args.showImages! == .mentioned {
|
||||
switch thread.backtrace {
|
||||
case let .raw(backtrace):
|
||||
for frame in backtrace.frames {
|
||||
let address = frame.adjustedProgramCounter
|
||||
if let imageNdx = target.images.firstIndex(
|
||||
where: { address >= $0.baseAddress
|
||||
&& address < $0.endOfText }
|
||||
) {
|
||||
mentionedImages.insert(imageNdx)
|
||||
}
|
||||
}
|
||||
case let .symbolicated(backtrace):
|
||||
for frame in backtrace.frames {
|
||||
if let symbol = frame.symbol, symbol.imageIndex >= 0 {
|
||||
mentionedImages.insert(symbol.imageIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if args.threads! {
|
||||
write(#", "threads": [ "#)
|
||||
for (ndx, thread) in target.threads.enumerated() {
|
||||
outputJSONThread(ndx: ndx, thread: thread)
|
||||
}
|
||||
write("]")
|
||||
} else {
|
||||
outputJSONThread(ndx: target.crashingThreadNdx,
|
||||
thread: crashingThread)
|
||||
}
|
||||
|
||||
if args.registers! == .crashedOnly {
|
||||
if let context = target.threads[target.crashingThreadNdx].context {
|
||||
write(#", "registers": { "#)
|
||||
outputJSONRegisterDump(context)
|
||||
write(" }")
|
||||
}
|
||||
}
|
||||
|
||||
if !capturedBytes.isEmpty && !(args.sanitize ?? false) {
|
||||
write(#"", capturedMemory": {"#)
|
||||
var first = true
|
||||
for (address, bytes) in capturedBytes {
|
||||
let formattedBytes = bytes
|
||||
.map{ hex($0, withPrefix: false) }
|
||||
.joined(separator: "")
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
write(",")
|
||||
}
|
||||
write(" \"\(hex(address))\": \"\(formattedBytes)\"")
|
||||
}
|
||||
write(" }, ")
|
||||
}
|
||||
|
||||
func outputJSONImage(_ image: Backtrace.Image, first: Bool) {
|
||||
if !first {
|
||||
write(", ")
|
||||
}
|
||||
|
||||
write("{ ")
|
||||
|
||||
if let name = image.name {
|
||||
write("\"name\": \"\(escapeJSON(name))\", ")
|
||||
}
|
||||
|
||||
if let bytes = image.uniqueID {
|
||||
let buildID = hex(bytes)
|
||||
write("\"buildId\": \"\(buildID)\", ")
|
||||
}
|
||||
|
||||
if var path = image.path {
|
||||
if args.sanitize ?? false {
|
||||
path = sanitizePath(path)
|
||||
}
|
||||
write("\"path\": \"\(path)\", ")
|
||||
}
|
||||
|
||||
write("""
|
||||
"baseAddress": "\(image.baseAddress)", \
|
||||
"endOfText": "\(image.endOfText)"
|
||||
""")
|
||||
|
||||
write(" }")
|
||||
}
|
||||
|
||||
switch args.showImages! {
|
||||
case .none:
|
||||
break
|
||||
case .mentioned:
|
||||
let images = mentionedImages.sorted().map{ target.images[$0] }
|
||||
let omitted = target.images.count - images.count
|
||||
write(", \"omittedImages\": \(omitted), \"images\": [ ")
|
||||
var first = true
|
||||
for image in images {
|
||||
outputJSONImage(image, first: first)
|
||||
if first {
|
||||
first = false
|
||||
}
|
||||
}
|
||||
write(" ] ")
|
||||
case .all:
|
||||
write(#", "images": [ "#)
|
||||
var first = true
|
||||
for image in target.images {
|
||||
outputJSONImage(image, first: first)
|
||||
if first {
|
||||
first = false
|
||||
}
|
||||
}
|
||||
write(" ] ")
|
||||
}
|
||||
|
||||
let secs = Double(backtraceDuration.tv_sec)
|
||||
+ 1.0e-9 * Double(backtraceDuration.tv_nsec)
|
||||
|
||||
write(", \"backtraceTime\": \(secs) ")
|
||||
|
||||
writeln("}")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,15 +34,32 @@ typealias CrashInfo = swift.runtime.backtrace.CrashInfo
|
||||
typealias thread = swift.runtime.backtrace.thread
|
||||
#endif
|
||||
|
||||
extension String {
|
||||
init<T: BinaryInteger>(_ value: T,
|
||||
width: Int,
|
||||
radix: Int = 10,
|
||||
uppercase: Bool = false) {
|
||||
let digits = String(value, radix: radix, uppercase: uppercase)
|
||||
if digits.count >= width {
|
||||
self = digits
|
||||
return
|
||||
}
|
||||
|
||||
let padding = String(repeating: "0",
|
||||
count: width - digits.count)
|
||||
self = padding + digits
|
||||
}
|
||||
}
|
||||
|
||||
internal func hex<T: FixedWidthInteger>(_ value: T,
|
||||
withPrefix: Bool = true) -> String {
|
||||
let digits = String(value, radix: 16)
|
||||
let padTo = value.bitWidth / 4
|
||||
let padding = digits.count >= padTo ? "" : String(repeating: "0",
|
||||
count: padTo - digits.count)
|
||||
let prefix = withPrefix ? "0x" : ""
|
||||
let formatted = String(value, width: value.bitWidth / 4, radix: 16)
|
||||
|
||||
return "\(prefix)\(padding)\(digits)"
|
||||
if withPrefix {
|
||||
return "0x" + formatted
|
||||
} else {
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
||||
internal func hex(_ bytes: [UInt8]) -> String {
|
||||
@@ -104,6 +121,7 @@ internal func recursiveRemoveContents(_ dir: String) throws {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a directory and its contents.
|
||||
internal func recursiveRemove(_ dir: String) throws {
|
||||
try recursiveRemoveContents(dir)
|
||||
|
||||
@@ -112,8 +130,25 @@ internal func recursiveRemove(_ dir: String) throws {
|
||||
}
|
||||
}
|
||||
|
||||
internal func withTemporaryDirectory(pattern: String, shouldDelete: Bool = true,
|
||||
body: (String) throws -> ()) throws {
|
||||
/// Run a closure, passing in the name of a temporary directory that will
|
||||
/// (optionally) be deleted automatically when the closure returns.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// - pattern: A string with some number of trailing 'X's giving the name
|
||||
/// to use for the directory; the 'X's will be replaced with a
|
||||
/// unique combination of alphanumeric characters.
|
||||
/// - shouldDelete: If `true` (the default), the directory and its contents
|
||||
/// will be removed automatically when the closure returns.
|
||||
/// - body: The closure to execute.
|
||||
///
|
||||
/// Returns:
|
||||
///
|
||||
/// This function returns whatever the closure returns.
|
||||
internal func withTemporaryDirectory<R>(
|
||||
pattern: String, shouldDelete: Bool = true,
|
||||
body: (String) throws -> R
|
||||
) throws -> R {
|
||||
var buf = Array<UInt8>(pattern.utf8)
|
||||
buf.append(0)
|
||||
|
||||
@@ -132,9 +167,10 @@ internal func withTemporaryDirectory(pattern: String, shouldDelete: Bool = true,
|
||||
}
|
||||
}
|
||||
|
||||
try body(dir)
|
||||
return try body(dir)
|
||||
}
|
||||
|
||||
/// Start a program with the specified arguments
|
||||
internal func spawn(_ path: String, args: [String]) throws {
|
||||
var cargs = args.map{ strdup($0) }
|
||||
cargs.append(nil)
|
||||
@@ -151,6 +187,7 @@ internal func spawn(_ path: String, args: [String]) throws {
|
||||
|
||||
#endif // os(macOS)
|
||||
|
||||
/// Test if the specified path is a directory
|
||||
internal func isDir(_ path: String) -> Bool {
|
||||
var st = stat()
|
||||
guard stat(path, &st) == 0 else {
|
||||
@@ -159,6 +196,7 @@ internal func isDir(_ path: String) -> Bool {
|
||||
return (st.st_mode & S_IFMT) == S_IFDIR
|
||||
}
|
||||
|
||||
/// Test if the specified path exists
|
||||
internal func exists(_ path: String) -> Bool {
|
||||
var st = stat()
|
||||
guard stat(path, &st) == 0 else {
|
||||
@@ -197,3 +235,52 @@ struct CFileStream: TextOutputStream {
|
||||
|
||||
var standardOutput = CFileStream(fp: stdout)
|
||||
var standardError = CFileStream(fp: stderr)
|
||||
|
||||
/// Format a timespec as an ISO8601 date/time
|
||||
func formatISO8601(_ time: timespec) -> String {
|
||||
var exploded = tm()
|
||||
var secs = time.tv_sec
|
||||
|
||||
gmtime_r(&secs, &exploded)
|
||||
|
||||
let isoTime = """
|
||||
\(String(exploded.tm_year + 1900, width: 4))-\
|
||||
\(String(exploded.tm_mon + 1, width: 2))-\
|
||||
\(String(exploded.tm_mday, width: 2))T\
|
||||
\(String(exploded.tm_hour, width: 2)):\
|
||||
\(String(exploded.tm_min, width: 2)):\
|
||||
\(String(exploded.tm_sec, width: 2)).\
|
||||
\(String(time.tv_nsec / 1000, width: 6))Z
|
||||
"""
|
||||
|
||||
return isoTime
|
||||
}
|
||||
|
||||
/// Escape a JSON string
|
||||
func escapeJSON(_ s: String) -> String {
|
||||
var result = ""
|
||||
let utf8View = s.utf8
|
||||
var chunk = utf8View.startIndex
|
||||
var pos = chunk
|
||||
let end = utf8View.endIndex
|
||||
|
||||
while pos != end {
|
||||
let scalar = utf8View[pos]
|
||||
switch scalar {
|
||||
case 0x22, 0x5c, 0x00...0x1f:
|
||||
result += s[chunk..<pos]
|
||||
result += "\\"
|
||||
result += String(Unicode.Scalar(scalar))
|
||||
pos = utf8View.index(after: pos)
|
||||
chunk = pos
|
||||
default:
|
||||
pos = utf8View.index(after: pos)
|
||||
}
|
||||
}
|
||||
|
||||
if chunk != end {
|
||||
result += s[chunk..<pos]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the Swift project authors
|
||||
// Copyright (c) 2023-25 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
|
||||
@@ -65,6 +65,11 @@ internal struct SwiftBacktrace {
|
||||
case full
|
||||
}
|
||||
|
||||
enum OutputFormat {
|
||||
case text
|
||||
case json
|
||||
}
|
||||
|
||||
struct Arguments {
|
||||
var unwindAlgorithm: UnwindAlgorithm = .precise
|
||||
var demangle = false
|
||||
@@ -82,6 +87,7 @@ internal struct SwiftBacktrace {
|
||||
var cache = true
|
||||
var outputTo: OutputTo = .stdout
|
||||
var symbolicate: Symbolication = .full
|
||||
var format: OutputFormat = .text
|
||||
var outputPath: String = "/tmp"
|
||||
}
|
||||
|
||||
@@ -91,6 +97,9 @@ internal struct SwiftBacktrace {
|
||||
static var target: Target? = nil
|
||||
static var currentThread: Int = 0
|
||||
|
||||
static var now = timespec(tv_sec: 0, tv_nsec: 0)
|
||||
static var backtraceDuration = timespec(tv_sec: 0, tv_nsec: 0)
|
||||
|
||||
static var theme: any Theme {
|
||||
if args.color {
|
||||
return Themes.color
|
||||
@@ -209,6 +218,9 @@ Generate a backtrace for the parent process.
|
||||
the path points to a directory, a unique filename will
|
||||
be generated automatically.
|
||||
|
||||
--format <format> Set the output format. Options are "text" and "json";
|
||||
the default is "text".
|
||||
|
||||
--crashinfo <addr>
|
||||
-a <addr> Provide a pointer to a platform specific CrashInfo
|
||||
structure. <addr> should be in hexadecimal.
|
||||
@@ -447,6 +459,23 @@ Generate a backtrace for the parent process.
|
||||
} else {
|
||||
args.symbolicate = .full
|
||||
}
|
||||
case "--format":
|
||||
if let v = value {
|
||||
switch v.lowercased() {
|
||||
case "text":
|
||||
args.format = .text
|
||||
case "json":
|
||||
args.format = .json
|
||||
default:
|
||||
print("swift-backtrace: unknown output format '\(v)'",
|
||||
to: &standardError)
|
||||
}
|
||||
} else {
|
||||
print("swift-backtrace: missing format value",
|
||||
to: &standardError)
|
||||
usage()
|
||||
exit(1)
|
||||
}
|
||||
default:
|
||||
print("swift-backtrace: unknown argument '\(arg)'",
|
||||
to: &standardError)
|
||||
@@ -530,7 +559,7 @@ Generate a backtrace for the parent process.
|
||||
|
||||
// Target's initializer fetches and symbolicates backtraces, so
|
||||
// we want to time that part here.
|
||||
let duration = measureDuration {
|
||||
backtraceDuration = measureDuration {
|
||||
target = Target(crashInfoAddr: crashInfoAddr,
|
||||
limit: args.limit, top: args.top,
|
||||
cache: args.cache,
|
||||
@@ -539,6 +568,13 @@ Generate a backtrace for the parent process.
|
||||
currentThread = target!.crashingThreadNdx
|
||||
}
|
||||
|
||||
// Grab the current wall clock time; if clock_gettime() fails, get a
|
||||
// lower resolution version instead.
|
||||
if clock_gettime(CLOCK_REALTIME, &now) != 0 {
|
||||
now.tv_sec = time(nil)
|
||||
now.tv_nsec = 0
|
||||
}
|
||||
|
||||
// Set up the output stream
|
||||
var didOpenOutput = false
|
||||
switch args.outputTo {
|
||||
@@ -553,21 +589,25 @@ Generate a backtrace for the parent process.
|
||||
let pid = target!.pid
|
||||
var now = timespec(tv_sec: 0, tv_nsec: 0)
|
||||
|
||||
if clock_gettime(CLOCK_REALTIME, &now) != 0 {
|
||||
now.tv_sec = time(nil)
|
||||
now.tv_nsec = 0
|
||||
let ext: String
|
||||
switch args.format {
|
||||
case .text:
|
||||
ext = "log"
|
||||
case .json:
|
||||
ext = "json"
|
||||
}
|
||||
|
||||
var filename =
|
||||
"\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec).log"
|
||||
"\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec).\(ext)"
|
||||
|
||||
var fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644)
|
||||
var ndx = 1
|
||||
|
||||
while fd < 0 && (errno == EEXIST || errno == EINTR) {
|
||||
if errno != EINTR {
|
||||
ndx += 1
|
||||
}
|
||||
filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec)-\(ndx).log"
|
||||
filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec)-\(ndx).\(ext)"
|
||||
fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644)
|
||||
}
|
||||
|
||||
@@ -604,14 +644,33 @@ Generate a backtrace for the parent process.
|
||||
}
|
||||
}
|
||||
|
||||
printCrashLog()
|
||||
// Clear (or complete) the message written by the crash handler; this
|
||||
// is always on stdout or stderr, even if you specify a file for output.
|
||||
var handlerOut: CFileStream
|
||||
if args.outputTo == .stdout {
|
||||
handlerOut = standardOutput
|
||||
} else {
|
||||
handlerOut = standardError
|
||||
}
|
||||
if args.color {
|
||||
print("\r\u{1b}[0K", terminator: "", to: &handlerOut)
|
||||
} else {
|
||||
print(" done ***\n\n", terminator: "", to: &handlerOut)
|
||||
}
|
||||
|
||||
writeln("")
|
||||
switch args.format {
|
||||
case .text:
|
||||
printCrashLog()
|
||||
|
||||
let formattedDuration = format(duration: duration)
|
||||
writeln("")
|
||||
|
||||
writeln("Backtrace took \(formattedDuration)s")
|
||||
writeln("")
|
||||
let formattedDuration = format(duration: backtraceDuration)
|
||||
|
||||
writeln("Backtrace took \(formattedDuration)s")
|
||||
writeln("")
|
||||
case .json:
|
||||
outputJSONCrashLog()
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
// On Darwin, if Developer Mode is turned off, or we can't tell if it's
|
||||
@@ -771,20 +830,6 @@ Generate a backtrace for the parent process.
|
||||
description = "Program crashed: \(target.signalDescription) at \(hex(target.faultAddress))"
|
||||
}
|
||||
|
||||
// Clear (or complete) the message written by the crash handler; this
|
||||
// is always on stdout or stderr, even if you specify a file for output.
|
||||
var handlerOut: CFileStream
|
||||
if args.outputTo == .stdout {
|
||||
handlerOut = standardOutput
|
||||
} else {
|
||||
handlerOut = standardError
|
||||
}
|
||||
if args.color {
|
||||
print("\r\u{1b}[0K", terminator: "", to: &handlerOut)
|
||||
} else {
|
||||
print(" done ***\n\n", terminator: "", to: &handlerOut)
|
||||
}
|
||||
|
||||
writeln(theme.crashReason(description))
|
||||
|
||||
var mentionedImages = Set<Int>()
|
||||
@@ -1329,11 +1374,15 @@ Generate a backtrace for the parent process.
|
||||
from: RemoteMemoryReader.Address(value),
|
||||
count: 16,
|
||||
as: UInt8.self) {
|
||||
let formattedBytes = theme.data(bytes.map{
|
||||
hex($0, withPrefix: false)
|
||||
}.joined(separator: " "))
|
||||
let printedBytes = printableBytes(from: bytes)
|
||||
writeln("\(reg) \(hexValue) \(formattedBytes) \(printedBytes)")
|
||||
if args.sanitize ?? false {
|
||||
writeln("\(reg) \(hexValue) <memory>")
|
||||
} else {
|
||||
let formattedBytes = theme.data(bytes.map{
|
||||
hex($0, withPrefix: false)
|
||||
}.joined(separator: " "))
|
||||
let printedBytes = printableBytes(from: bytes)
|
||||
writeln("\(reg) \(hexValue) \(formattedBytes) \(printedBytes)")
|
||||
}
|
||||
} else {
|
||||
let decValue = theme.decimalValue("\(value)")
|
||||
writeln("\(reg) \(hexValue) \(decValue)")
|
||||
|
||||
@@ -120,7 +120,7 @@ SWIFT_RUNTIME_STDLIB_INTERNAL BacktraceSettings _swift_backtraceSettings = {
|
||||
// top
|
||||
16,
|
||||
|
||||
// sanitize,
|
||||
// sanitize
|
||||
SanitizePaths::Preset,
|
||||
|
||||
// preset
|
||||
@@ -129,7 +129,7 @@ SWIFT_RUNTIME_STDLIB_INTERNAL BacktraceSettings _swift_backtraceSettings = {
|
||||
// cache
|
||||
true,
|
||||
|
||||
// outputTo,
|
||||
// outputTo
|
||||
OutputTo::Auto,
|
||||
|
||||
// symbolicate
|
||||
@@ -138,6 +138,9 @@ SWIFT_RUNTIME_STDLIB_INTERNAL BacktraceSettings _swift_backtraceSettings = {
|
||||
// suppressWarnings
|
||||
false,
|
||||
|
||||
// format
|
||||
OutputFormat::Text,
|
||||
|
||||
// swiftBacktracePath
|
||||
NULL,
|
||||
|
||||
@@ -769,6 +772,16 @@ _swift_processBacktracingSetting(llvm::StringRef key,
|
||||
}
|
||||
} else if (key.equals_insensitive("symbolicate")) {
|
||||
_swift_backtraceSettings.symbolicate = parseSymbolication(value);
|
||||
} else if (key.equals_insensitive("format")) {
|
||||
if (value.equals_insensitive("text")) {
|
||||
_swift_backtraceSettings.format = OutputFormat::Text;
|
||||
} else if (value.equals_insensitive("json")) {
|
||||
_swift_backtraceSettings.format = OutputFormat::JSON;
|
||||
} else {
|
||||
swift::warning(0,
|
||||
"swift runtime: unknown backtrace format '%.*s'\n",
|
||||
static_cast<int>(value.size()), value.data());
|
||||
}
|
||||
#if !defined(SWIFT_RUNTIME_FIXED_BACKTRACER_PATH)
|
||||
} else if (key.equals_insensitive("swift-backtrace")) {
|
||||
size_t len = value.size();
|
||||
@@ -1088,6 +1101,8 @@ const char *backtracer_argv[] = {
|
||||
"stdout", // 30
|
||||
"--symbolicate", // 31
|
||||
"full", // 32
|
||||
"--format", // 33
|
||||
"text", // 34
|
||||
NULL
|
||||
};
|
||||
|
||||
@@ -1225,6 +1240,15 @@ _swift_spawnBacktracer(CrashInfo *crashInfo)
|
||||
break;
|
||||
}
|
||||
|
||||
switch (_swift_backtraceSettings.format) {
|
||||
case OutputFormat::Text:
|
||||
backtracer_argv[34] = "text";
|
||||
break;
|
||||
case OutputFormat::JSON:
|
||||
backtracer_argv[34] = "json";
|
||||
break;
|
||||
}
|
||||
|
||||
_swift_formatUnsigned(_swift_backtraceSettings.timeout, timeout_buf);
|
||||
|
||||
if (_swift_backtraceSettings.limit < 0)
|
||||
|
||||
@@ -104,6 +104,11 @@ enum class Symbolication {
|
||||
Full = 2,
|
||||
};
|
||||
|
||||
enum class OutputFormat {
|
||||
Text = 0,
|
||||
JSON = 1
|
||||
};
|
||||
|
||||
struct BacktraceSettings {
|
||||
UnwindAlgorithm algorithm;
|
||||
OnOffTty enabled;
|
||||
@@ -122,6 +127,7 @@ struct BacktraceSettings {
|
||||
OutputTo outputTo;
|
||||
Symbolication symbolicate;
|
||||
bool suppressWarnings;
|
||||
OutputFormat format;
|
||||
const char *swiftBacktracePath;
|
||||
const char *outputPath;
|
||||
};
|
||||
|
||||
59
test/Backtracing/JSON.swift
Normal file
59
test/Backtracing/JSON.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
// RUN: %empty-directory(%t)
|
||||
// RUN: %target-build-swift %s -parse-as-library -Onone -g -o %t/JSON
|
||||
// RUN: %target-codesign %t/JSON
|
||||
// RUN: (env SWIFT_BACKTRACE=enable=yes,cache=no %target-run %t/JSON 2>&1 || true) | %FileCheck %s
|
||||
|
||||
// UNSUPPORTED: use_os_stdlib
|
||||
// UNSUPPORTED: back_deployment_runtime
|
||||
// UNSUPPORTED: asan
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: backtracing
|
||||
// REQUIRES: OS=macosx || OS=linux-gnu
|
||||
|
||||
func level1() {
|
||||
level2()
|
||||
}
|
||||
|
||||
func level2() {
|
||||
level3()
|
||||
}
|
||||
|
||||
func level3() {
|
||||
level4()
|
||||
}
|
||||
|
||||
func level4() {
|
||||
level5()
|
||||
}
|
||||
|
||||
func level5() {
|
||||
print("About to crash")
|
||||
let ptr = UnsafeMutablePointer<Int>(bitPattern: 4)!
|
||||
ptr.pointee = 42
|
||||
}
|
||||
|
||||
@main
|
||||
struct JSON {
|
||||
static func main() {
|
||||
level1()
|
||||
}
|
||||
}
|
||||
|
||||
// CHECK: *** Program crashed: Bad pointer dereference at 0x{{0+}}4 ***
|
||||
|
||||
// CHECK: Thread 0 {{(".*" )?}}crashed:
|
||||
|
||||
// CHECK: 0 0x{{[0-9a-f]+}} level5() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:42:15
|
||||
// CHECK-NEXT: 1 [ra] 0x{{[0-9a-f]+}} level4() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:36:3
|
||||
// CHECK-NEXT: 2 [ra] 0x{{[0-9a-f]+}} level3() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:32:3
|
||||
// CHECK-NEXT: 3 [ra] 0x{{[0-9a-f]+}} level2() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:28:3
|
||||
// CHECK-NEXT: 4 [ra] 0x{{[0-9a-f]+}} level1() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:24:3
|
||||
// CHECK-NEXT: 5 [ra] 0x{{[0-9a-f]+}} static JSON.main() + {{[0-9]+}} in JSON at {{.*}}/JSON.swift:48:5
|
||||
// CHECK-NEXT: 6 [ra] [system] 0x{{[0-9a-f]+}} static JSON.$main() + {{[0-9]+}} in JSON at {{.*}}/<compiler-generated>
|
||||
// CHECK-NEXT: 7 [ra] [system] 0x{{[0-9a-f]+}} main + {{[0-9]+}} in JSON at {{.*}}/JSON.swift
|
||||
|
||||
// CHECK: Registers:
|
||||
|
||||
// CHECK: Images ({{[0-9]+}} omitted):
|
||||
|
||||
// CHECK: {{0x[0-9a-f]+}}–{{0x[0-9a-f]+}}{{ +}}{{([0-9a-f]+|<no build ID>)}}{{ +}}JSON{{ +}}{{.*}}/JSON
|
||||
Reference in New Issue
Block a user