//===-- ProcessDebugger.cpp -------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "ProcessDebugger.h"

// Windows includes
#include "lldb/Host/windows/windows.h"
#include <psapi.h>

#include "lldb/Host/FileSystem.h"
#include "lldb/Host/HostNativeProcessBase.h"
#include "lldb/Host/HostProcess.h"
#include "lldb/Host/HostThread.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "lldb/Target/MemoryRegionInfo.h"
#include "lldb/Target/Process.h"
#include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/Error.h"

#include "DebuggerThread.h"
#include "ExceptionRecord.h"
#include "ProcessWindowsLog.h"

using namespace lldb;
using namespace lldb_private;

static DWORD ConvertLldbToWinApiProtect(uint32_t protect) {
  // We also can process a read / write permissions here, but if the debugger
  // will make later a write into the allocated memory, it will fail. To get
  // around it is possible inside DoWriteMemory to remember memory permissions,
  // allow write, write and restore permissions, but for now we process only
  // the executable permission.
  //
  // TODO: Process permissions other than executable
  if (protect & ePermissionsExecutable)
    return PAGE_EXECUTE_READWRITE;

  return PAGE_READWRITE;
}

// The Windows page protection bits are NOT independent masks that can be
// bitwise-ORed together.  For example, PAGE_EXECUTE_READ is not (PAGE_EXECUTE
// | PAGE_READ).  To test for an access type, it's necessary to test for any of
// the bits that provide that access type.
static bool IsPageReadable(uint32_t protect) {
  return (protect & PAGE_NOACCESS) == 0;
}

static bool IsPageWritable(uint32_t protect) {
  return (protect & (PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY |
                     PAGE_READWRITE | PAGE_WRITECOPY)) != 0;
}

static bool IsPageExecutable(uint32_t protect) {
  return (protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE |
                     PAGE_EXECUTE_WRITECOPY)) != 0;
}

namespace lldb_private {

lldb::pid_t ProcessDebugger::GetDebuggedProcessId() const {
  if (m_session_data)
    return m_session_data->m_debugger->GetProcess().GetProcessId();
  return LLDB_INVALID_PROCESS_ID;
}

Status ProcessDebugger::DetachProcess() {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);
  DebuggerThreadSP debugger_thread;
  {
    // Acquire the lock only long enough to get the DebuggerThread.
    // StopDebugging() will trigger a call back into ProcessDebugger which will
    // also acquire the lock.  Thus we have to release the lock before calling
    // StopDebugging().
    llvm::sys::ScopedLock lock(m_mutex);

    if (!m_session_data) {
      LLDB_LOG(log, "there is no active session.");
      return Status();
    }

    debugger_thread = m_session_data->m_debugger;
  }

  Status error;

  LLDB_LOG(log, "detaching from process {0}.",
           debugger_thread->GetProcess().GetNativeProcess().GetSystemHandle());
  error = debugger_thread->StopDebugging(false);

  // By the time StopDebugging returns, there is no more debugger thread, so
  // we can be assured that no other thread will race for the session data.
  m_session_data.reset();

  return error;
}

Status ProcessDebugger::LaunchProcess(ProcessLaunchInfo &launch_info,
                                      DebugDelegateSP delegate) {
  // Even though m_session_data is accessed here, it is before a debugger
  // thread has been kicked off.  So there's no race conditions, and it
  // shouldn't be necessary to acquire the mutex.

  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);
  Status result;

  FileSpec working_dir = launch_info.GetWorkingDirectory();
  namespace fs = llvm::sys::fs;
  if (working_dir) {
    FileSystem::Instance().Resolve(working_dir);
    if (!FileSystem::Instance().IsDirectory(working_dir)) {
      result.SetErrorStringWithFormat("No such file or directory: %s",
                                      working_dir.GetCString());
      return result;
    }
  }

  if (!launch_info.GetFlags().Test(eLaunchFlagDebug)) {
    StreamString stream;
    stream.Printf("ProcessDebugger unable to launch '%s'.  ProcessDebugger can "
                  "only be used for debug launches.",
                  launch_info.GetExecutableFile().GetPath().c_str());
    std::string message = stream.GetString();
    result.SetErrorString(message.c_str());

    LLDB_LOG(log, "error: {0}", message);
    return result;
  }

  bool stop_at_entry = launch_info.GetFlags().Test(eLaunchFlagStopAtEntry);
  m_session_data.reset(new ProcessWindowsData(stop_at_entry));
  m_session_data->m_debugger.reset(new DebuggerThread(delegate));
  DebuggerThreadSP debugger = m_session_data->m_debugger;

  // Kick off the DebugLaunch asynchronously and wait for it to complete.
  result = debugger->DebugLaunch(launch_info);
  if (result.Fail()) {
    LLDB_LOG(log, "failed launching '{0}'. {1}",
             launch_info.GetExecutableFile().GetPath(), result);
    return result;
  }

  HostProcess process;
  Status error = WaitForDebuggerConnection(debugger, process);
  if (error.Fail()) {
    LLDB_LOG(log, "failed launching '{0}'. {1}",
             launch_info.GetExecutableFile().GetPath(), error);
    return error;
  }

  LLDB_LOG(log, "successfully launched '{0}'",
           launch_info.GetExecutableFile().GetPath());

  // We've hit the initial stop.  If eLaunchFlagsStopAtEntry was specified, the
  // private state should already be set to eStateStopped as a result of
  // hitting the initial breakpoint.  If it was not set, the breakpoint should
  // have already been resumed from and the private state should already be
  // eStateRunning.
  launch_info.SetProcessID(process.GetProcessId());

  return result;
}

Status ProcessDebugger::AttachProcess(lldb::pid_t pid,
                                      const ProcessAttachInfo &attach_info,
                                      DebugDelegateSP delegate) {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);
  m_session_data.reset(
      new ProcessWindowsData(!attach_info.GetContinueOnceAttached()));
  DebuggerThreadSP debugger(new DebuggerThread(delegate));

  m_session_data->m_debugger = debugger;

  DWORD process_id = static_cast<DWORD>(pid);
  Status error = debugger->DebugAttach(process_id, attach_info);
  if (error.Fail()) {
    LLDB_LOG(
        log,
        "encountered an error occurred initiating the asynchronous attach. {0}",
        error);
    return error;
  }

  HostProcess process;
  error = WaitForDebuggerConnection(debugger, process);
  if (error.Fail()) {
    LLDB_LOG(log,
             "encountered an error waiting for the debugger to connect. {0}",
             error);
    return error;
  }

  LLDB_LOG(log, "successfully attached to process with pid={0}", process_id);

  // We've hit the initial stop.  If eLaunchFlagsStopAtEntry was specified, the
  // private state should already be set to eStateStopped as a result of
  // hitting the initial breakpoint.  If it was not set, the breakpoint should
  // have already been resumed from and the private state should already be
  // eStateRunning.

  return error;
}

Status ProcessDebugger::DestroyProcess(const lldb::StateType state) {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);
  DebuggerThreadSP debugger_thread;
  {
    // Acquire this lock inside an inner scope, only long enough to get the
    // DebuggerThread. StopDebugging() will trigger a call back into
    // ProcessDebugger which will acquire the lock again, so we need to not
    // deadlock.
    llvm::sys::ScopedLock lock(m_mutex);

    if (!m_session_data) {
      LLDB_LOG(log, "warning: state = {0}, but there is no active session.",
               state);
      return Status();
    }

    debugger_thread = m_session_data->m_debugger;
  }

  Status error;
  if (state != eStateExited && state != eStateDetached) {
    LLDB_LOG(
        log, "Shutting down process {0}.",
        debugger_thread->GetProcess().GetNativeProcess().GetSystemHandle());
    error = debugger_thread->StopDebugging(true);

    // By the time StopDebugging returns, there is no more debugger thread, so
    // we can be assured that no other thread will race for the session data.
    m_session_data.reset();
  } else {
    error.SetErrorStringWithFormat("cannot destroy process %" PRIx64
                                   " while state = %d",
                                   GetDebuggedProcessId(), state);
    LLDB_LOG(log, "error: {0}", error);
  }
  return error;
}

Status ProcessDebugger::HaltProcess(bool &caused_stop) {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);
  Status error;
  llvm::sys::ScopedLock lock(m_mutex);
  caused_stop = ::DebugBreakProcess(m_session_data->m_debugger->GetProcess()
                                        .GetNativeProcess()
                                        .GetSystemHandle());
  if (!caused_stop) {
    error.SetError(::GetLastError(), eErrorTypeWin32);
    LLDB_LOG(log, "DebugBreakProcess failed with error {0}", error);
  }

  return error;
}

Status ProcessDebugger::ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
                                   size_t &bytes_read) {
  Status error;
  bytes_read = 0;
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_MEMORY);
  llvm::sys::ScopedLock lock(m_mutex);

  if (!m_session_data) {
    error.SetErrorString(
        "cannot read, there is no active debugger connection.");
    LLDB_LOG(log, "error: {0}", error);
    return error;
  }

  LLDB_LOG(log, "attempting to read {0} bytes from address {1:x}", size,
           vm_addr);

  HostProcess process = m_session_data->m_debugger->GetProcess();
  void *addr = reinterpret_cast<void *>(vm_addr);
  SIZE_T num_of_bytes_read = 0;
  if (!::ReadProcessMemory(process.GetNativeProcess().GetSystemHandle(), addr,
                           buf, size, &num_of_bytes_read)) {
    // Reading from the process can fail for a number of reasons - set the
    // error code and make sure that the number of bytes read is set back to 0
    // because in some scenarios the value of bytes_read returned from the API
    // is garbage.
    error.SetError(GetLastError(), eErrorTypeWin32);
    LLDB_LOG(log, "reading failed with error: {0}", error);
  } else {
    bytes_read = num_of_bytes_read;
  }
  return error;
}

Status ProcessDebugger::WriteMemory(lldb::addr_t vm_addr, const void *buf,
                                    size_t size, size_t &bytes_written) {
  Status error;
  bytes_written = 0;
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_MEMORY);
  llvm::sys::ScopedLock lock(m_mutex);
  LLDB_LOG(log, "attempting to write {0} bytes into address {1:x}", size,
           vm_addr);

  if (!m_session_data) {
    error.SetErrorString(
        "cannot write, there is no active debugger connection.");
    LLDB_LOG(log, "error: {0}", error);
    return error;
  }

  HostProcess process = m_session_data->m_debugger->GetProcess();
  void *addr = reinterpret_cast<void *>(vm_addr);
  SIZE_T num_of_bytes_written = 0;
  lldb::process_t handle = process.GetNativeProcess().GetSystemHandle();
  if (::WriteProcessMemory(handle, addr, buf, size, &num_of_bytes_written)) {
    FlushInstructionCache(handle, addr, num_of_bytes_written);
    bytes_written = num_of_bytes_written;
  } else {
    error.SetError(GetLastError(), eErrorTypeWin32);
    LLDB_LOG(log, "writing failed with error: {0}", error);
  }
  return error;
}

Status ProcessDebugger::AllocateMemory(size_t size, uint32_t permissions,
                                       lldb::addr_t &addr) {
  Status error;
  addr = LLDB_INVALID_ADDRESS;
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_MEMORY);
  llvm::sys::ScopedLock lock(m_mutex);
  LLDB_LOG(log, "attempting to allocate {0} bytes with permissions {1}", size,
           permissions);

  if (!m_session_data) {
    error.SetErrorString(
        "cannot allocate, there is no active debugger connection");
    LLDB_LOG(log, "error: {0}", error);
    return error;
  }

  HostProcess process = m_session_data->m_debugger->GetProcess();
  lldb::process_t handle = process.GetNativeProcess().GetSystemHandle();
  auto protect = ConvertLldbToWinApiProtect(permissions);
  auto result = ::VirtualAllocEx(handle, nullptr, size, MEM_COMMIT, protect);
  if (!result) {
    error.SetError(GetLastError(), eErrorTypeWin32);
    LLDB_LOG(log, "allocating failed with error: {0}", error);
  } else {
    addr = reinterpret_cast<addr_t>(result);
  }
  return error;
}

Status ProcessDebugger::DeallocateMemory(lldb::addr_t vm_addr) {
  Status result;

  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_MEMORY);
  llvm::sys::ScopedLock lock(m_mutex);
  LLDB_LOG(log, "attempting to deallocate bytes at address {0}", vm_addr);

  if (!m_session_data) {
    result.SetErrorString(
        "cannot deallocate, there is no active debugger connection");
    LLDB_LOG(log, "error: {0}", result);
    return result;
  }

  HostProcess process = m_session_data->m_debugger->GetProcess();
  lldb::process_t handle = process.GetNativeProcess().GetSystemHandle();
  if (!::VirtualFreeEx(handle, reinterpret_cast<LPVOID>(vm_addr), 0,
                       MEM_RELEASE)) {
    result.SetError(GetLastError(), eErrorTypeWin32);
    LLDB_LOG(log, "deallocating failed with error: {0}", result);
  }

  return result;
}

Status ProcessDebugger::GetMemoryRegionInfo(lldb::addr_t vm_addr,
                                            MemoryRegionInfo &info) {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_MEMORY);
  Status error;
  llvm::sys::ScopedLock lock(m_mutex);
  info.Clear();

  if (!m_session_data) {
    error.SetErrorString(
        "GetMemoryRegionInfo called with no debugging session.");
    LLDB_LOG(log, "error: {0}", error);
    return error;
  }
  HostProcess process = m_session_data->m_debugger->GetProcess();
  lldb::process_t handle = process.GetNativeProcess().GetSystemHandle();
  if (handle == nullptr || handle == LLDB_INVALID_PROCESS) {
    error.SetErrorString(
        "GetMemoryRegionInfo called with an invalid target process.");
    LLDB_LOG(log, "error: {0}", error);
    return error;
  }

  LLDB_LOG(log, "getting info for address {0:x}", vm_addr);

  void *addr = reinterpret_cast<void *>(vm_addr);
  MEMORY_BASIC_INFORMATION mem_info = {};
  SIZE_T result = ::VirtualQueryEx(handle, addr, &mem_info, sizeof(mem_info));
  if (result == 0) {
    if (::GetLastError() == ERROR_INVALID_PARAMETER) {
      // ERROR_INVALID_PARAMETER is returned if VirtualQueryEx is called with
      // an address past the highest accessible address. We should return a
      // range from the vm_addr to LLDB_INVALID_ADDRESS
      info.GetRange().SetRangeBase(vm_addr);
      info.GetRange().SetRangeEnd(LLDB_INVALID_ADDRESS);
      info.SetReadable(MemoryRegionInfo::eNo);
      info.SetExecutable(MemoryRegionInfo::eNo);
      info.SetWritable(MemoryRegionInfo::eNo);
      info.SetMapped(MemoryRegionInfo::eNo);
      return error;
    } else {
      error.SetError(::GetLastError(), eErrorTypeWin32);
      LLDB_LOG(log,
               "VirtualQueryEx returned error {0} while getting memory "
               "region info for address {1:x}",
               error, vm_addr);
      return error;
    }
  }

  // Protect bits are only valid for MEM_COMMIT regions.
  if (mem_info.State == MEM_COMMIT) {
    const bool readable = IsPageReadable(mem_info.Protect);
    const bool executable = IsPageExecutable(mem_info.Protect);
    const bool writable = IsPageWritable(mem_info.Protect);
    info.SetReadable(readable ? MemoryRegionInfo::eYes : MemoryRegionInfo::eNo);
    info.SetExecutable(executable ? MemoryRegionInfo::eYes
                                  : MemoryRegionInfo::eNo);
    info.SetWritable(writable ? MemoryRegionInfo::eYes : MemoryRegionInfo::eNo);
  } else {
    info.SetReadable(MemoryRegionInfo::eNo);
    info.SetExecutable(MemoryRegionInfo::eNo);
    info.SetWritable(MemoryRegionInfo::eNo);
  }

  // AllocationBase is defined for MEM_COMMIT and MEM_RESERVE but not MEM_FREE.
  if (mem_info.State != MEM_FREE) {
    info.GetRange().SetRangeBase(
        reinterpret_cast<addr_t>(mem_info.AllocationBase));
    info.GetRange().SetRangeEnd(reinterpret_cast<addr_t>(mem_info.BaseAddress) +
                                mem_info.RegionSize);
    info.SetMapped(MemoryRegionInfo::eYes);
  } else {
    // In the unmapped case we need to return the distance to the next block of
    // memory. VirtualQueryEx nearly does that except that it gives the
    // distance from the start of the page containing vm_addr.
    SYSTEM_INFO data;
    ::GetSystemInfo(&data);
    DWORD page_offset = vm_addr % data.dwPageSize;
    info.GetRange().SetRangeBase(vm_addr);
    info.GetRange().SetByteSize(mem_info.RegionSize - page_offset);
    info.SetMapped(MemoryRegionInfo::eNo);
  }

  error.SetError(::GetLastError(), eErrorTypeWin32);
  LLDB_LOGV(log,
            "Memory region info for address {0}: readable={1}, "
            "executable={2}, writable={3}",
            vm_addr, info.GetReadable(), info.GetExecutable(),
            info.GetWritable());
  return error;
}

void ProcessDebugger::OnExitProcess(uint32_t exit_code) {
  // If the process exits before any initial stop then notify the debugger
  // of the error otherwise WaitForDebuggerConnection() will be blocked.
  // An example of this issue is when a process fails to load a dependent DLL.
  if (m_session_data && !m_session_data->m_initial_stop_received) {
    Status error(exit_code, eErrorTypeWin32);
    OnDebuggerError(error, 0);
  }
}

void ProcessDebugger::OnDebuggerConnected(lldb::addr_t image_base) {}

ExceptionResult
ProcessDebugger::OnDebugException(bool first_chance,
                                  const ExceptionRecord &record) {
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_EXCEPTION);
  llvm::sys::ScopedLock lock(m_mutex);
  // FIXME: Without this check, occasionally when running the test suite
  // there is an issue where m_session_data can be null.  It's not clear how
  // this could happen but it only surfaces while running the test suite.  In
  // order to properly diagnose this, we probably need to first figure allow the
  // test suite to print out full lldb logs, and then add logging to the process
  // plugin.
  if (!m_session_data) {
    LLDB_LOG(log,
             "Debugger thread reported exception {0:x} at address {1:x}, but "
             "there is no session.",
             record.GetExceptionCode(), record.GetExceptionAddress());
    return ExceptionResult::SendToApplication;
  }

  ExceptionResult result = ExceptionResult::SendToApplication;
  if ((record.GetExceptionCode() == EXCEPTION_BREAKPOINT ||
       record.GetExceptionCode() ==
           0x4000001FL /*WOW64 STATUS_WX86_BREAKPOINT*/) &&
      !m_session_data->m_initial_stop_received) {
    // Handle breakpoints at the first chance.
    result = ExceptionResult::BreakInDebugger;
    LLDB_LOG(
        log,
        "Hit loader breakpoint at address {0:x}, setting initial stop event.",
        record.GetExceptionAddress());
    m_session_data->m_initial_stop_received = true;
    ::SetEvent(m_session_data->m_initial_stop_event);
  }
  return result;
}

void ProcessDebugger::OnCreateThread(const HostThread &thread) {
  // Do nothing by default
}

void ProcessDebugger::OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) {
  // Do nothing by default
}

void ProcessDebugger::OnLoadDll(const ModuleSpec &module_spec,
                                lldb::addr_t module_addr) {
  // Do nothing by default
}

void ProcessDebugger::OnUnloadDll(lldb::addr_t module_addr) {
  // Do nothing by default
}

void ProcessDebugger::OnDebugString(const std::string &string) {}

void ProcessDebugger::OnDebuggerError(const Status &error, uint32_t type) {
  llvm::sys::ScopedLock lock(m_mutex);
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS);

  if (m_session_data->m_initial_stop_received) {
    // This happened while debugging.  Do we shutdown the debugging session,
    // try to continue, or do something else?
    LLDB_LOG(log,
             "Error {0} occurred during debugging.  Unexpected behavior "
             "may result.  {1}",
             error.GetError(), error);
  } else {
    // If we haven't actually launched the process yet, this was an error
    // launching the process.  Set the internal error and signal the initial
    // stop event so that the DoLaunch method wakes up and returns a failure.
    m_session_data->m_launch_error = error;
    ::SetEvent(m_session_data->m_initial_stop_event);
    LLDB_LOG(log,
             "Error {0} occurred launching the process before the initial "
             "stop. {1}",
             error.GetError(), error);
    return;
  }
}

Status ProcessDebugger::WaitForDebuggerConnection(DebuggerThreadSP debugger,
                                                  HostProcess &process) {
  Status result;
  Log *log = ProcessWindowsLog::GetLogIfAny(WINDOWS_LOG_PROCESS |
                                            WINDOWS_LOG_BREAKPOINTS);
  LLDB_LOG(log, "Waiting for loader breakpoint.");

  // Block this function until we receive the initial stop from the process.
  if (::WaitForSingleObject(m_session_data->m_initial_stop_event, INFINITE) ==
      WAIT_OBJECT_0) {
    LLDB_LOG(log, "hit loader breakpoint, returning.");

    process = debugger->GetProcess();
    return m_session_data->m_launch_error;
  } else
    return Status(::GetLastError(), eErrorTypeWin32);
}

} // namespace lldb_private
