//===--- TargetWindows.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 Windows version.
//
//===----------------------------------------------------------------------===//

#if os(Windows)

import WinSDK
import CRT

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

internal import BacktracingImpl.OS.Windows
internal import BacktracingImpl.OS.WinNTInternals

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

struct TargetThread {
  typealias ThreadID = DWORD

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

fileprivate func GetProcessImageFileName(_ hProcess: HANDLE) -> String? {
  return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: 2048) {
    buffer in

    let dwRet = K32GetProcessImageFileNameW(hProcess, buffer.baseAddress,
                                            DWORD(buffer.count))
    if dwRet == 0 {
      return nil
    }

    let slice = buffer[0..<Int(dwRet)]

    return String(decoding: slice, as: UTF16.self)
  }
}

fileprivate func GetThreadDescription(_ hThread: HANDLE) -> String? {
  var description: PWSTR? = nil

  let result = GetThreadDescription(hThread, &description)
  if result != 0 {
    return nil
  }

  return String.decodeCString(description!, as: UTF16.self)?.result
}

fileprivate func getProcessName(_ hProcess: HANDLE) -> String? {
  // According to Microsoft's documentation, using GetModuleBaseName()
  // for this is unreliable, and we should use GetProcessImageFileName()
  // instead, then process the returned path.
  //
  // See https://learn.microsoft.com/en-gb/windows/win32/api/psapi/nf-psapi-getmodulebasenamew

  guard let filename = GetProcessImageFileName(hProcess) else {
    return nil
  }

  if let ndx = filename.lastIndex(of: "\\"), ndx < filename.endIndex {
    return String(filename.suffix(from: filename.index(after: ndx)))
  } else {
    return filename
  }
}

let ntDll = GetModuleHandleA("ntdll.dll")
let ntQuerySystemInformation = unsafeBitCast(GetProcAddress(ntDll, "NtQuerySystemInformation")!, to: PNT_QUERY_SYSTEM_INFORMATION.self)
let ntSuspendProcess = unsafeBitCast(GetProcAddress(ntDll, "NtSuspendProcess")!, to: PNT_SUSPEND_PROCESS.self)
let ntResumeProcess = unsafeBitCast(GetProcAddress(ntDll, "NtResumeProcess")!, to: PNT_RESUME_PROCESS.self)

fileprivate func threadIdsForProcess(_ processId: DWORD) -> [DWORD]? {
  var bufferSize: ULONG = 0

  let status = ntQuerySystemInformation(SystemProcessInformation,
                                        nil,
                                        0,
                                        &bufferSize)
  if status != 0 && status != NTSTATUS(bitPattern: STATUS_INFO_LENGTH_MISMATCH) {
    return nil
  }

  var threadIds: [DWORD]? = nil
  while true {
    let status =
      withUnsafeTemporaryAllocation(
        of: UInt8.self, capacity: Int(bufferSize)
      ) { buffer in
        let status = ntQuerySystemInformation(SystemProcessInformation,
                                              buffer.baseAddress,
                                              ULONG(buffer.count),
                                              &bufferSize)

        if status != 0 {
          return status
        }

        var offset = 0
        let totalSize = Int(bufferSize)
        let spiSize = MemoryLayout<SYSTEM_PROCESS_INFORMATION>.size
        while threadIds == nil
                && offset < totalSize
                && totalSize - offset >= spiSize {
          let slice = UnsafeBufferPointer(rebasing:
                                            buffer[offset..<offset+spiSize])
          slice.withMemoryRebound(to: SYSTEM_PROCESS_INFORMATION.self) {
            info in

            if DWORD(UInt(bitPattern: info[0].UniqueProcessId)) == processId {
              // If this is the process we're interested in, find the thread
              // information
              let threadInfoOffset = offset + spiSize
              let stiSize = MemoryLayout<SYSTEM_THREAD_INFORMATION>.stride
              let stiTotal = stiSize * Int(info[0].NumberOfThreads)

              if threadInfoOffset + stiTotal <= totalSize {
                let slice = buffer[threadInfoOffset..<threadInfoOffset+stiTotal]
                var result: [DWORD] = []

                UnsafeBufferPointer(rebasing:slice).withMemoryRebound(
                  to: SYSTEM_THREAD_INFORMATION.self
                ) { threads in
                  for thread in threads {
                    result.append(DWORD(UInt(bitPattern:
                                               thread.ClientId.UniqueThread)))
                  }
                }

                threadIds = result
              }
            }

            let nextOffset = Int(info[0].NextEntryOffset)
            if nextOffset == 0 {
              offset = totalSize
            } else {
              offset += nextOffset
            }
          }
        }

        return status
      }

    if status == 0 {
      return threadIds
    } else if status == NTSTATUS(bitPattern: STATUS_INFO_LENGTH_MISMATCH) {
      continue
    } else {
      return nil
    }
  }
}

class Target {
  typealias Address = UInt64

  var hProcess: HANDLE = INVALID_HANDLE_VALUE
  var hThreads: [HANDLE] = []

  var pid: DWORD
  var name: String
  var crashingThread: TargetThread.ThreadID
  var exceptionCode: DWORD
  var faultAddress: Address
  var exceptionInfo: Address

  var images: ImageMap

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

  var signalDescription: String {
    switch exceptionCode {
      case EXCEPTION_ACCESS_VIOLATION: return "Access violation"
      case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "Array bounds exceeded"
      case EXCEPTION_BREAKPOINT: return "Breakpoint"
      case EXCEPTION_DATATYPE_MISALIGNMENT: return "Data type misalignment"
      case EXCEPTION_FLT_DENORMAL_OPERAND: return "Denormal floating point"
      case EXCEPTION_FLT_INEXACT_RESULT: return "Inexact floating point result"
      case EXCEPTION_FLT_INVALID_OPERATION:
        return "Invalid floating point operation"
      case EXCEPTION_FLT_OVERFLOW: return "Floating point overflow"
      case EXCEPTION_FLT_STACK_CHECK: return "Floating point stack check failed"
      case EXCEPTION_FLT_UNDERFLOW: return "Floating point underflow"
      case EXCEPTION_GUARD_PAGE: return "Guard page"
      case EXCEPTION_ILLEGAL_INSTRUCTION: return "Illegal instruction"
      case EXCEPTION_IN_PAGE_ERROR: return "Page fault"
      case EXCEPTION_INT_DIVIDE_BY_ZERO: return "Integer divide by zero"
      case EXCEPTION_INT_OVERFLOW: return "Integer overflow"
      case EXCEPTION_INVALID_DISPOSITION: return "Invalid disposition"
      case EXCEPTION_INVALID_HANDLE: return "Invalid handle"
      case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "Noncontinuable exception"
      case EXCEPTION_PRIV_INSTRUCTION: return "Privileged instruction"
      case EXCEPTION_SINGLE_STEP: return "Single step"
      case EXCEPTION_STACK_OVERFLOW: return "Stack overflow"
      default:
        return "Exception \(hex(exceptionCode))"
    }
  }

  var reader: RemoteMemoryReader

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

    guard let hProcess = OpenProcess(
            DWORD(
              PROCESS_VM_READ
              | PROCESS_QUERY_LIMITED_INFORMATION
              | PROCESS_SUSPEND_RESUME
            ),
            false,
            pid
          ) else {
      let error = GetLastError()
      print("swift-backtrace: unable to open process: \(hex(error)).",
            to: &standardError)
      exit(1)
    }

    let suspendStatus = ntSuspendProcess(hProcess)
    if suspendStatus != 0 {
      print("swift-backtrace: unable to suspend process: \(hex(suspendStatus)).",
            to: &standardError)
      exit(1)
    }

    guard let threads = threadIdsForProcess(pid) else {
      print("swift-backtrace: unable to get thread list.",
            to: &standardError)
      exit(1)
    }

    for thread in threads {
      guard let hThread = OpenThread(
              DWORD(
                THREAD_GET_CONTEXT
                  | THREAD_QUERY_LIMITED_INFORMATION
                  | THREAD_SUSPEND_RESUME
              ),
              false,
              thread
            ) else {
        let error = GetLastError()
        print("swift-backtrace: unable to open thread \(thread): \(hex(error)).",
              to: &standardError)
        continue
      }

      hThreads.append(hThread)
      SuspendThread(hThread)
    }

    let resumeStatus = ntResumeProcess(hProcess)
    if resumeStatus != 0 {
      print("swift-backtrace: unable to resume process: \(hex(resumeStatus)).",
            to: &standardError)
    }

    if hThreads.count < 1 {
      print("swift-backtrace: no threads.", to: &standardError)
      exit(1)
    }

    reader = RemoteMemoryReader(hProcess: UInt(bitPattern: hProcess))

    if let theName = getProcessName(hProcess) {
      name = theName
    } else {
      print("swift-backtrace: unable to fetch process name.", to: &standardError)
      exit(1)
    }

    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 = DWORD(truncatingIfNeeded: crashInfo.crashing_thread)
    exceptionCode = DWORD(crashInfo.signal)
    faultAddress = Address(truncatingIfNeeded: crashInfo.fault_address)
    exceptionInfo = Address(truncatingIfNeeded: crashInfo.exception_info)

    images = ImageMap.capture(for: UInt(bitPattern: hProcess))

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

  func fetchThreads(
    limit: Int?, top: Int, cache: Bool,
    symbolicate: SwiftBacktrace.Symbolication
  ) {
    for hThread in hThreads {
      let id = GetThreadId(hThread)
      let name = GetThreadDescription(hThread) ?? ""

      // Grab the NT context and convert to our type
      var ctx = CONTEXT()

      let context = withUnsafeMutablePointer(to: &ctx) {
        (lpctx: UnsafeMutablePointer<CONTEXT>) -> HostContext? in
        if id == crashingThread {
          do {
            let ppctx = exceptionInfo + Address(MemoryLayout<UInt>.size)
            let pctx = try reader.fetch(from: ppctx, as: UInt.self)
            lpctx.pointee = try reader.fetch(from: Address(pctx),
                                             as: CONTEXT.self)
          } catch {
            return nil
          }
        } else if !GetThreadContext(hThread, lpctx) {
          return nil
        }

        return HostContext(ntContext: UnsafeRawPointer(lpctx))
      }

      guard let context else {
        let err = GetLastError()
        print("swift-backtrace: failed to get thread context: \(hex(err))",
              to: &standardError)
        continue
      }

      guard let backtrace = try? Backtrace.capture(from: context,
                                                   using: reader,
                                                   images: images,
                                                   algorithm: .auto,
                                                   limit: limit,
                                                   offset: 0,
                                                   top: top) else {
        print("swift-backtrace: unable to capture backtrace from context for thread \(id)",
              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 id == crashingThread {
        crashingThreadNdx = threads.count
        ResumeThread(hThread)
      }

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

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

  func redoBacktraces(
    limit: Int?, top: Int,
    cache: Bool,
    symbolicate: SwiftBacktrace.Symbolication
  ) {
    for (ndx, thread) in threads.enumerated() {
      guard let backtrace = try? Backtrace.capture(from: thread.context!,
                                                   using: reader,
                                                   images: images,
                                                   algorithm: .auto,
                                                   limit: limit,
                                                   offset: 0,
                                                   top: top) else {
        print("swift-backtrace: unable to capture backtrace from context for thread \(thread.id)",
              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 \(thread.id)",
                to: &standardError)
          continue
        }

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

  enum DebuggerResult {
    case abort
    case handOffToDebugger
  }

  func withDebugger(_ body: () -> DebuggerResult) throws {
    let cmdline = """
      cmd.exe "/c echo Once LLDB has attached, \
      return to the other window and press any key \
      & echo. \
      & lldb --attach-pid \(pid) -o c \
      & pause "
      """
    let title = "Debugging \(name) (\(pid))"

    var startupInfo = STARTUPINFOW()
    startupInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
    var processInfo = PROCESS_INFORMATION()

    let ret =
      title.withCString(encodedAs: UTF16.self) { pwszTitle in
        // Not really mutating
        startupInfo.lpTitle = UnsafeMutablePointer(mutating: pwszTitle)

        return cmdline.withCString(encodedAs: UTF16.self) { pwszCmdline in
          return CreateProcessW(nil,
                                // Not really mutating
                                UnsafeMutablePointer(mutating: pwszCmdline),
                                nil,
                                nil,
                                false,
                                DWORD(NORMAL_PRIORITY_CLASS
                                      | CREATE_NEW_CONSOLE
                                      | CREATE_NEW_PROCESS_GROUP),
                                nil,
                                nil,
                                &startupInfo,
                                &processInfo)
        }
      }

    if !ret {
      let err = GetLastError()
      print("swift-backtrace: unable to spawn debugger: \(hex(err))")
    } else {
      CloseHandle(processInfo.hProcess)
      CloseHandle(processInfo.hThread)

      if body() == .handOffToDebugger {
        for (ndx,hThread) in hThreads.enumerated() {
          if ndx != crashingThreadNdx {
            ResumeThread(hThread)
          }
        }
        exit(0)
      }

    }
  }
}

#endif // os(Windows)

