//===--- TargetMacOS.swift - Represents a process we are inspecting -------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2022-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
//
//===----------------------------------------------------------------------===//
//
//  Defines `Target`, which represents the process we are inspecting.
//  This is the macOS version.
//
//===----------------------------------------------------------------------===//

#if os(macOS)

import Darwin
import Darwin.Mach

import Runtime
@_spi(Internal) import Runtime
@_spi(Contexts) import Runtime
@_spi(MemoryReaders) import Runtime

internal import BacktracingImpl.OS.Darwin
internal import BacktracingImpl.Runtime

#if arch(x86_64)
typealias MContext = darwin_x86_64_mcontext
#elseif arch(arm64) || arch(arm64_32)
typealias MContext = darwin_arm64_mcontext
#else
#error("You need to define MContext for this architecture")
#endif

extension thread_extended_info {
  var pth_swiftName: String {
    withUnsafePointer(to: pth_name) { ptr in
      let len = strnlen(ptr, Int(MAXTHREADNAMESIZE))
      return String(decoding: UnsafeRawBufferPointer(start: ptr, count: Int(len)),
                    as: UTF8.self)
    }
  }
}

enum SomeBacktrace {
  case raw(Backtrace)
  case symbolicated(SymbolicatedBacktrace)
}

struct TargetThread {
  typealias ThreadID = UInt64

  var id: ThreadID
  var context: HostContext?
  var name: String
  var backtrace: SomeBacktrace
}

class Target {
  typealias Address = UInt64

  var pid: pid_t
  var name: String
  var signal: UInt64
  var faultAddress: Address
  var crashingThread: TargetThread.ThreadID

  var task: task_t
  var images: ImageMap

  var threads: [TargetThread] = []
  var crashingThreadNdx: Int = -1

  var signalName: String {
    switch signal {
      case UInt64(SIGQUIT): return "SIGQUIT"
      case UInt64(SIGABRT): return "SIGABRT"
      case UInt64(SIGBUS):  return "SIGBUS"
      case UInt64(SIGFPE): return "SIGFPE"
      case UInt64(SIGILL): return "SIGILL"
      case UInt64(SIGSEGV): return "SIGSEGV"
      case UInt64(SIGTRAP): return "SIGTRAP"
      default: return "\(signal)"
    }
  }

  var signalDescription: String {
    switch signal {
      case UInt64(SIGQUIT): return "Terminated"
      case UInt64(SIGABRT): return "Aborted"
      case UInt64(SIGBUS): return "Bus error"
      case UInt64(SIGFPE): return "Floating point exception"
      case UInt64(SIGILL): return "Illegal instruction"
      case UInt64(SIGSEGV): return "Bad pointer dereference"
      case UInt64(SIGTRAP): return "System trap"
      default:
        return "Signal \(signal)"
    }
  }

  var reader: RemoteMemoryReader

  var mcontext: MContext

  static func getTask(pid: pid_t) -> task_t? {
    var port: task_t = 0
    let kr = task_read_for_pid(mach_task_self_, pid, &port)
    if kr != KERN_SUCCESS {
      return nil
    }
    return port
  }

  static func getProcessName(pid: pid_t) -> String {
    let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: 4096)
    defer {
      buffer.deallocate()
    }
    let ret = proc_name(pid, buffer.baseAddress, UInt32(buffer.count))
    if ret <= 0 {
      return "<unknown>"
    } else {
      return String(decoding: buffer[0..<Int(ret)], as: UTF8.self)
    }
  }

  static func isPrivileged(pid: pid_t) -> Bool {
    var flags = UInt32(0)

    guard csops(pid,
                UInt32(CS_OPS_STATUS),
                &flags,
                MemoryLayout<UInt32>.size) == 0 else {
      return true
    }

    if (flags & UInt32(CS_PLATFORM_BINARY | CS_PLATFORM_PATH | CS_RUNTIME)) != 0 {
      return true
    }

    return (flags & UInt32(CS_GET_TASK_ALLOW)) == 0
  }

  init(crashInfoAddr: UInt64, limit: Int?, top: Int, cache: Bool,
       symbolicate: SwiftBacktrace.Symbolication) {
    pid = getppid()

    if Self.isPrivileged(pid: pid) {
      /* Exit silently in this case; either

         1. We can't call csops(), because we're sandboxed, or
         2. The target is a platform binary.

         If we get killed, that is also fine. */
      exit(1)
    }

    // This will normally only succeed if the parent process has
    // the com.apple.security.get-task-allow privilege.  That gets set
    // automatically if you're developing in Xcode; if you're developing
    // on the command line or using SwiftPM, you will need to code sign
    // your binary with that entitlement to get this to work.
    guard let parentTask = Self.getTask(pid: pid) else {
      exit(1)
    }

    task = parentTask

    reader = RemoteMemoryReader(task: task_t(task))

    name = Self.getProcessName(pid: pid)

    let crashInfo: CrashInfo
    do {
      crashInfo = try reader.fetch(from: crashInfoAddr, as: CrashInfo.self)
    } catch {
      print("swift-backtrace: unable to fetch crash info.", to: &standardError)
      exit(1)
    }

    crashingThread = crashInfo.crashing_thread
    signal = crashInfo.signal
    faultAddress = crashInfo.fault_address

    guard let mctx: MContext = try? reader.fetch(from: crashInfo.mctx,
                                                 as: MContext.self) else {
      print("swift-backtrace: unable to fetch mcontext.", to: &standardError)
      exit(1)
    }

    mcontext = mctx

    images = ImageMap.capture(for: task)

    fetchThreads(limit: limit, top: top, cache: cache, symbolicate: symbolicate)
  }

  func fetchThreads(
    limit: Int?, top: Int, cache: Bool,
    symbolicate: SwiftBacktrace.Symbolication
  ) {
    var threadPorts: thread_act_array_t? = nil
    var threadCount: mach_msg_type_number_t = 0
    let kr = task_threads(task,
                          &threadPorts,
                          &threadCount)

    if kr != KERN_SUCCESS {
      print("swift-backtrace: failed to enumerate threads - \(kr)",
            to: &standardError)
      exit(1)
    }

    guard let ports = threadPorts else {
      print("swift-backtrace: thread array is nil", to: &standardError)
      exit(1)
    }

    for ndx in 0..<threadCount {
      var threadIdInfo: thread_identifier_info_data_t? = nil
      var kr = mach_thread_info(ports[Int(ndx)], THREAD_IDENTIFIER_INFO,
                                &threadIdInfo)
      if kr != KERN_SUCCESS {
        print("swift-backtrace: unable to get thread info for thread \(ndx) - \(kr)",
              to: &standardError)
        exit(1)
      }

      guard let info = threadIdInfo else {
        print("swift-backtrace: thread info is nil", to: &standardError)
        exit(1)
      }

      var extInfo = thread_extended_info_data_t()

      let threadName: String

      kr = mach_thread_info(ports[Int(ndx)],
                            THREAD_EXTENDED_INFO,
                            &extInfo)
      if kr == KERN_SUCCESS {
        threadName = extInfo.pth_swiftName
      } else {
        print("swift-backtrace: unable to fetch ext info \(kr)",
              to: &standardError)
        threadName = ""
      }

      let ctx: HostContext

      if info.thread_id == crashingThread {
        ctx = HostContext.fromHostMContext(mcontext)
        crashingThreadNdx = threads.count
      } else {
        guard let threadCtx = HostContext.fromHostThread(ports[Int(ndx)]) else {
          // This can happen legitimately, e.g. when looking at a Rosetta 2
          // process, where there are ARM64 threads AS WELL AS the x86_64 ones.
          mach_port_deallocate(mach_task_self_, ports[Int(ndx)])
          continue
        }
        ctx = threadCtx
      }

      guard let backtrace = try? Backtrace.capture(from: ctx,
                                                   using: reader,
                                                   images: nil,
                                                   algorithm: .auto,
                                                   limit: limit,
                                                   offset: 0,
                                                   top: top) else {
        print("swift-backtrace: unable to capture backtrace from context for thread \(ndx)",
              to: &standardError)
        exit(1)
      }

      let shouldSymbolicate: Bool
      var options: Backtrace.SymbolicationOptions
      switch symbolicate {
        case .off:
          shouldSymbolicate = false
          options = []
        case .fast:
          shouldSymbolicate = true
          options = [ .showSourceLocations ]
        case .full:
          shouldSymbolicate = true
          options = [ .showInlineFrames, .showSourceLocations ]
      }

      if cache {
        options.insert(.useSymbolCache)
      }

      if shouldSymbolicate {
        guard let symbolicated = backtrace.symbolicated(
                with: images,
                options: options
              ) else {
          print("unable to symbolicate backtrace from context for thread \(ndx)",
                to: &standardError)
          exit(1)
        }

        threads.append(TargetThread(id: info.thread_id,
                                    context: ctx,
                                    name: threadName,
                                    backtrace: .symbolicated(symbolicated)))
      } else {
        threads.append(TargetThread(id: info.thread_id,
                                    context: ctx,
                                    name: threadName,
                                    backtrace: .raw(backtrace)))
      }

      mach_port_deallocate(mach_task_self_, ports[Int(ndx)])
    }
  }

  public func redoBacktraces(
    limit: Int?, top: Int,
    cache: Bool,
    symbolicate: SwiftBacktrace.Symbolication
  ) {
    for (ndx, thread) in threads.enumerated() {
      guard let context = thread.context else {
        continue
      }

      guard let backtrace = try? Backtrace.capture(from: context,
                                                   using: reader,
                                                   images: nil,
                                                   algorithm: .auto,
                                                   limit: limit,
                                                   offset: 0,
                                                   top: top) else {
        print("swift-backtrace: unable to capture backtrace from context for thread \(ndx)",
              to: &standardError)
        continue
      }

      let shouldSymbolicate: Bool
      var options: Backtrace.SymbolicationOptions
      switch symbolicate {
        case .off:
          shouldSymbolicate = false
          options = []
        case .fast:
          shouldSymbolicate = true
          options = [ .showSourceLocations ]
        case .full:
          shouldSymbolicate = true
          options = [ .showInlineFrames, .showSourceLocations ]
      }

      if cache {
        options.insert(.useSymbolCache)
      }

      if shouldSymbolicate {
        guard let symbolicated = backtrace.symbolicated(
                with: images,
                options: options
              ) else {
          print("swift-backtrace: unable to symbolicate backtrace from context for thread \(ndx)",
                to: &standardError)
          continue
        }

        threads[ndx].backtrace = .symbolicated(symbolicated)
      } else {
        threads[ndx].backtrace = .raw(backtrace)
      }
    }
  }

  enum DebuggerResult {
    case abort
    case handOffToDebugger
  }

  public func withDebugger(_ body: () -> DebuggerResult) throws {
    #if os(macOS)
    return try withTemporaryDirectory(pattern: "/tmp/backtrace.XXXXXXXX") {
      tmpdir in

      let cmdfile = "\(tmpdir)/lldb.command"
      guard let fp = fopen(cmdfile, "wt") else {
        throw PosixError(errno: errno)
      }
      if fputs("""
                 #!/bin/bash
                 clear
                 echo "Once LLDB has attached, return to the other window and press any key"
                 echo ""
                 xcrun lldb --attach-pid \(pid) -o c
                 """, fp) == EOF {
        throw PosixError(errno: errno)
      }
      if fclose(fp) != 0 {
        throw PosixError(errno: errno)
      }
      if chmod(cmdfile, S_IXUSR|S_IRUSR) != 0 {
        throw PosixError(errno: errno)
      }

      try spawn("/usr/bin/open", args: ["open", cmdfile])

      if body() == .handOffToDebugger {
        exit(0)
      }
    }
    #else
    print("""
            From another shell, please run

            lldb --attach-pid \(pid) -o c
            """)
    body()
    #endif
  }
}

private func mach_thread_info<T>(_ thread: thread_t,
                                 _ flavor: CInt,
                                 _ result: inout T) -> kern_return_t {
  var count: mach_msg_type_number_t
    = mach_msg_type_number_t(MemoryLayout<T>.stride
                               / MemoryLayout<natural_t>.stride)

  return withUnsafeMutablePointer(to: &result) { ptr in
    ptr.withMemoryRebound(to: natural_t.self, capacity: Int(count)) { intPtr in
      return thread_info(thread,
                         thread_flavor_t(flavor),
                         intPtr,
                         &count)
    }
  }
}

#endif // os(macOS)
