// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/debug/wasm/gdb-server/target.h"

#include <inttypes.h>
#include "src/base/platform/time.h"
#include "src/debug/wasm/gdb-server/gdb-remote-util.h"
#include "src/debug/wasm/gdb-server/gdb-server.h"
#include "src/debug/wasm/gdb-server/packet.h"
#include "src/debug/wasm/gdb-server/session.h"
#include "src/debug/wasm/gdb-server/transport.h"

namespace v8 {
namespace internal {
namespace wasm {
namespace gdb_server {

static const int kThreadId = 1;

// Signals.
static const int kSigTrace = 5;
static const int kSigSegv = 11;

Target::Target(GdbServer* gdb_server)
    : gdb_server_(gdb_server),
      status_(Status::Running),
      cur_signal_(0),
      session_(nullptr),
      debugger_initial_suspension_(true),
      semaphore_(0),
      current_isolate_(nullptr) {
  InitQueryPropertyMap();
}

void Target::InitQueryPropertyMap() {
  // Request LLDB to send packets up to 4000 bytes for bulk transfers.
  query_properties_["Supported"] =
      "PacketSize=1000;vContSupported-;qXfer:libraries:read+;wasm+;";

  query_properties_["Attached"] = "1";

  // There is only one register, named 'pc', in this architecture
  query_properties_["RegisterInfo0"] =
      "name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:"
      "General Purpose Registers;gcc:16;dwarf:16;generic:pc;";
  query_properties_["RegisterInfo1"] = "E45";

  // ProcessInfo for wasm32
  query_properties_["ProcessInfo"] =
      "pid:1;ppid:1;uid:1;gid:1;euid:1;egid:1;name:6c6c6462;triple:" +
      Mem2Hex("wasm32-unknown-unknown-wasm") + ";ptrsize:4;";
  query_properties_["Symbol"] = "OK";

  // Current thread info
  char buff[16];
  snprintf(buff, sizeof(buff), "QC%x", kThreadId);
  query_properties_["C"] = buff;
}

void Target::Terminate() {
  // Executed in the Isolate thread, when the process shuts down.
  SetStatus(Status::Terminated);
}

void Target::OnProgramBreak(Isolate* isolate,
                            const std::vector<wasm_addr_t>& call_frames) {
  OnSuspended(isolate, kSigTrace, call_frames);
}
void Target::OnException(Isolate* isolate,
                         const std::vector<wasm_addr_t>& call_frames) {
  OnSuspended(isolate, kSigSegv, call_frames);
}
void Target::OnSuspended(Isolate* isolate, int signal,
                         const std::vector<wasm_addr_t>& call_frames) {
  // This function will be called in the isolate thread, when the wasm
  // interpreter gets suspended.

  bool isWaitingForSuspension = (status_ == Status::WaitingForSuspension);
  SetStatus(Status::Suspended, signal, call_frames, isolate);
  if (isWaitingForSuspension) {
    // Wake the GdbServer thread that was blocked waiting for the Target
    // to suspend.
    semaphore_.Signal();
  } else if (session_) {
    session_->SignalThreadEvent();
  }
}

void Target::Run(Session* session) {
  // Executed in the GdbServer thread.
  session_ = session;
  do {
    WaitForDebugEvent();
    ProcessDebugEvent();
    ProcessCommands();
  } while (!IsTerminated() && session_->IsConnected());
  session_ = nullptr;
}

void Target::WaitForDebugEvent() {
  // Executed in the GdbServer thread.

  if (status_ == Status::Running) {
    // Wait for either:
    //   * the thread to fault (or single-step)
    //   * an interrupt from LLDB
    session_->WaitForDebugStubEvent();
  }
}

void Target::ProcessDebugEvent() {
  // Executed in the GdbServer thread

  if (status_ == Status::Running) {
    // Blocks, waiting for the engine to suspend.
    Suspend();
  }

  // Here, the wasm interpreter has suspended and we have updated the current
  // thread info.

  if (debugger_initial_suspension_) {
    // First time on a connection, we don't send the signal.
    // All other times, send the signal that triggered us.
    debugger_initial_suspension_ = false;
  } else {
    Packet pktOut;
    SetStopReply(&pktOut);
    session_->SendPacket(&pktOut, false);
  }
}

void Target::Suspend() {
  // Executed in the GdbServer thread
  if (status_ == Status::Running) {
    // TODO(paolosev) - this only suspends the wasm interpreter.
    gdb_server_->Suspend();

    status_ = Status::WaitingForSuspension;
  }

  while (status_ == Status::WaitingForSuspension) {
    if (semaphore_.WaitFor(base::TimeDelta::FromMilliseconds(500))) {
      // Here the wasm interpreter is suspended.
      return;
    }
  }
}

void Target::ProcessCommands() {
  // GDB-remote messages are processed in the GDBServer thread.

  if (IsTerminated()) {
    return;
  } else if (status_ != Status::Suspended) {
    // Don't process commands if we haven't stopped.
    return;
  }

  // Now we are ready to process commands.
  // Loop through packets until we process a continue packet or a detach.
  Packet recv, reply;
  while (session_->IsConnected()) {
    if (!session_->GetPacket(&recv)) {
      continue;
    }

    reply.Clear();
    ProcessPacketResult result = ProcessPacket(&recv, &reply);
    switch (result) {
      case ProcessPacketResult::Paused:
        session_->SendPacket(&reply);
        break;

      case ProcessPacketResult::Continue:
        DCHECK_EQ(status_, Status::Running);
        // If this is a continue type command, break out of this loop.
        gdb_server_->QuitMessageLoopOnPause();
        return;

      case ProcessPacketResult::Detach:
        SetStatus(Status::Running);
        session_->SendPacket(&reply);
        session_->Disconnect();
        gdb_server_->QuitMessageLoopOnPause();
        return;

      case ProcessPacketResult::Kill:
        session_->SendPacket(&reply);
        exit(-9);

      default:
        UNREACHABLE();
    }
  }

  if (!session_->IsConnected()) {
    debugger_initial_suspension_ = true;
  }
}

Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
                                                  Packet* pkt_out) {
  ErrorCode err = ErrorCode::None;

  // Clear the outbound message.
  pkt_out->Clear();

  // Set the sequence number, if present.
  int32_t seq = -1;
  if (pkt_in->GetSequence(&seq)) {
    pkt_out->SetSequence(seq);
  }

  // A GDB-remote packet begins with an upper- or lower-case letter, which
  // generally represents a single command.
  // The letters 'q' and 'Q' introduce a "General query packets" and are used
  // to extend the protocol with custom commands.
  // The format of GDB-remote commands is documented here:
  // https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview.
  char cmd;
  pkt_in->GetRawChar(&cmd);

  switch (cmd) {
    // Queries the reason the target halted.
    // IN : $?
    // OUT: A Stop-reply packet
    case '?':
      SetStopReply(pkt_out);
      break;

    // Resumes execution
    // IN : $c
    // OUT: A Stop-reply packet is sent later, when the execution halts.
    case 'c':
      SetStatus(Status::Running);
      return ProcessPacketResult::Continue;

    // Detaches the debugger from this target
    // IN : $D
    // OUT: $OK
    case 'D':
      TRACE_GDB_REMOTE("Requested Detach.\n");
      pkt_out->AddString("OK");
      return ProcessPacketResult::Detach;

    // Read general registers (We only support register 'pc' that contains
    // the current instruction pointer).
    // IN : $g
    // OUT: $xx...xx
    case 'g': {
      uint64_t pc = GetCurrentPc();
      pkt_out->AddBlock(&pc, sizeof(pc));
      break;
    }

    // Write general registers - NOT SUPPORTED
    // IN : $Gxx..xx
    // OUT: $ (empty string)
    case 'G': {
      break;
    }

    // Set thread for subsequent operations. For Wasm targets, we currently
    // assume that there is only one thread with id = kThreadId (= 1).
    // IN : $H(c/g)(-1,0,xxxx)
    // OUT: $OK
    case 'H': {
      // Type of the operation (‘m’, ‘M’, ‘g’, ‘G’, ...)
      char operation;
      if (!pkt_in->GetRawChar(&operation)) {
        err = ErrorCode::BadFormat;
        break;
      }

      uint64_t thread_id;
      if (!pkt_in->GetNumberSep(&thread_id, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }

      // Ignore, only one thread supported for now.
      pkt_out->AddString("OK");
      break;
    }

    // Kills the debuggee.
    // IN : $k
    // OUT: $OK
    case 'k':
      TRACE_GDB_REMOTE("Requested Kill.\n");
      pkt_out->AddString("OK");
      return ProcessPacketResult::Kill;

    // Reads {llll} addressable memory units starting at address {aaaa}.
    // IN : $maaaa,llll
    // OUT: $xx..xx
    case 'm': {
      uint64_t address;
      if (!pkt_in->GetNumberSep(&address, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }
      wasm_addr_t wasm_addr(address);

      uint64_t len;
      if (!pkt_in->GetNumberSep(&len, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }

      if (len > Transport::kBufSize / 2) {
        err = ErrorCode::BadArgs;
        break;
      }

      uint32_t length = static_cast<uint32_t>(len);
      uint8_t buff[Transport::kBufSize];
      if (wasm_addr.ModuleId() > 0) {
        uint32_t read =
            gdb_server_->GetWasmModuleBytes(wasm_addr, buff, length);
        if (read > 0) {
          pkt_out->AddBlock(buff, read);
        } else {
          err = ErrorCode::Failed;
        }
      } else {
        err = ErrorCode::BadArgs;
      }
      break;
    }

    // Writes {llll} addressable memory units starting at address {aaaa}.
    // IN : $Maaaa,llll:xx..xx
    // OUT: $OK
    case 'M': {
      // Writing to memory not supported for Wasm.
      err = ErrorCode::Failed;
      break;
    }

    // pN: Reads the value of register N.
    // IN : $pxx
    // OUT: $xx..xx
    case 'p': {
      uint64_t pc = GetCurrentPc();
      pkt_out->AddBlock(&pc, sizeof(pc));
    } break;

    case 'q': {
      err = ProcessQueryPacket(pkt_in, pkt_out);
      break;
    }

    // Single step
    // IN : $s
    // OUT: A Stop-reply packet is sent later, when the execution halts.
    case 's': {
      if (status_ == Status::Suspended) {
        gdb_server_->PrepareStep();
        SetStatus(Status::Running);
      }
      return ProcessPacketResult::Continue;
    }

    // Find out if the thread 'id' is alive.
    // IN : $T
    // OUT: $OK if alive, $Enn if thread is dead.
    case 'T': {
      uint64_t id;
      if (!pkt_in->GetNumberSep(&id, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }
      if (id != kThreadId) {
        err = ErrorCode::BadArgs;
        break;
      }
      pkt_out->AddString("OK");
      break;
    }

    // Z: Adds a breakpoint
    // IN : $Z<type>,<addr>,<kind>
    //      <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
    // OUT: $OK (success) or $Enn (error)
    case 'Z': {
      uint64_t breakpoint_type;
      uint64_t breakpoint_address;
      uint64_t breakpoint_kind;
      // Only software breakpoints are supported.
      if (!pkt_in->GetNumberSep(&breakpoint_type, 0) || breakpoint_type != 0 ||
          !pkt_in->GetNumberSep(&breakpoint_address, 0) ||
          !pkt_in->GetNumberSep(&breakpoint_kind, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }

      wasm_addr_t wasm_breakpoint_addr(breakpoint_address);
      if (!gdb_server_->AddBreakpoint(wasm_breakpoint_addr.ModuleId(),
                                      wasm_breakpoint_addr.Offset())) {
        err = ErrorCode::Failed;
        break;
      }

      pkt_out->AddString("OK");
      break;
    }

    // z: Removes a breakpoint
    // IN : $z<type>,<addr>,<kind>
    //      <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
    // OUT: $OK (success) or $Enn (error)
    case 'z': {
      uint64_t breakpoint_type;
      uint64_t breakpoint_address;
      uint64_t breakpoint_kind;
      if (!pkt_in->GetNumberSep(&breakpoint_type, 0) || breakpoint_type != 0 ||
          !pkt_in->GetNumberSep(&breakpoint_address, 0) ||
          !pkt_in->GetNumberSep(&breakpoint_kind, 0)) {
        err = ErrorCode::BadFormat;
        break;
      }

      wasm_addr_t wasm_breakpoint_addr(breakpoint_address);
      if (!gdb_server_->RemoveBreakpoint(wasm_breakpoint_addr.ModuleId(),
                                         wasm_breakpoint_addr.Offset())) {
        err = ErrorCode::Failed;
        break;
      }

      pkt_out->AddString("OK");
      break;
    }

    // If the command is not recognized, ignore it by sending an empty reply.
    default: {
      TRACE_GDB_REMOTE("Unknown command: %s\n", pkt_in->GetPayload());
    }
  }

  // If there is an error, return the error code instead of a payload
  if (err != ErrorCode::None) {
    pkt_out->Clear();
    pkt_out->AddRawChar('E');
    pkt_out->AddWord8(static_cast<uint8_t>(err));
  }
  return ProcessPacketResult::Paused;
}

Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
                                             Packet* pkt_out) {
  const char* str = &pkt_in->GetPayload()[1];

  // Get first thread query
  // IN : $qfThreadInfo
  // OUT: $m<tid>
  //
  // Get next thread query
  // IN : $qsThreadInfo
  // OUT: $m<tid> or l to denote end of list.
  if (!strcmp(str, "fThreadInfo") || !strcmp(str, "sThreadInfo")) {
    if (str[0] == 'f') {
      pkt_out->AddString("m");
      pkt_out->AddNumberSep(kThreadId, 0);
    } else {
      pkt_out->AddString("l");
    }
    return ErrorCode::None;
  }

  // Get a list of loaded libraries
  // IN : $qXfer:libraries:read
  // OUT: an XML document which lists loaded libraries, with this format:
  // <library-list>
  //   <library name="foo.wasm">
  //     <section address="0x100000000"/>
  //   </library>
  //   <library name="bar.wasm">
  //     <section address="0x200000000"/>
  //   </library>
  // </library-list>
  // Note that LLDB must be compiled with libxml2 support to handle this packet.
  std::string tmp = "Xfer:libraries:read";
  if (!strncmp(str, tmp.data(), tmp.length())) {
    std::vector<GdbServer::WasmModuleInfo> modules =
        gdb_server_->GetLoadedModules(true);
    std::string result("l<library-list>");
    for (const auto& module : modules) {
      wasm_addr_t address(module.module_id, 0);
      char address_string[32];
      snprintf(address_string, sizeof(address_string), "%" PRIu64,
               static_cast<uint64_t>(address));
      result += "<library name=\"";
      result += module.module_name;
      result += "\"><section address=\"";
      result += address_string;
      result += "\"/></library>";
    }
    result += "</library-list>";
    pkt_out->AddString(result.c_str());
    return ErrorCode::None;
  }

  // Get the current call stack.
  // IN : $qWasmCallStack
  // OUT: $xx..xxyy..yyzz..zz (A sequence of uint64_t values represented as
  //                           consecutive 8-bytes blocks).
  std::vector<std::string> toks = StringSplit(str, ":;");
  if (toks[0] == "WasmCallStack") {
    std::vector<wasm_addr_t> call_stack_pcs = gdb_server_->GetWasmCallStack();
    std::vector<uint64_t> buffer;
    for (wasm_addr_t pc : call_stack_pcs) {
      buffer.push_back(pc);
    }
    pkt_out->AddBlock(buffer.data(),
                      static_cast<uint32_t>(sizeof(uint64_t) * buffer.size()));
    return ErrorCode::None;
  }

  // Get a Wasm global value in the Wasm module specified.
  // IN : $qWasmGlobal:frame_index;index
  // OUT: $xx..xx
  if (toks[0] == "WasmGlobal") {
    if (toks.size() == 3) {
      uint32_t frame_index =
          static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
      uint32_t index =
          static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
      uint8_t buff[16];
      uint32_t size = 0;
      if (gdb_server_->GetWasmGlobal(frame_index, index, buff, 16, &size)) {
        pkt_out->AddBlock(buff, size);
        return ErrorCode::None;
      } else {
        return ErrorCode::Failed;
      }
    }
    return ErrorCode::BadFormat;
  }

  // Get a Wasm local value in the stack frame specified.
  // IN : $qWasmLocal:frame_index;index
  // OUT: $xx..xx
  if (toks[0] == "WasmLocal") {
    if (toks.size() == 3) {
      uint32_t frame_index =
          static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
      uint32_t index =
          static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
      uint8_t buff[16];
      uint32_t size = 0;
      if (gdb_server_->GetWasmLocal(frame_index, index, buff, 16, &size)) {
        pkt_out->AddBlock(buff, size);
        return ErrorCode::None;
      } else {
        return ErrorCode::Failed;
      }
    }
    return ErrorCode::BadFormat;
  }

  // Get a Wasm local from the operand stack at the index specified.
  // IN : qWasmStackValue:frame_index;index
  // OUT: $xx..xx
  if (toks[0] == "WasmStackValue") {
    if (toks.size() == 3) {
      uint32_t frame_index =
          static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
      uint32_t index =
          static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
      uint8_t buff[16];
      uint32_t size = 0;
      if (gdb_server_->GetWasmStackValue(frame_index, index, buff, 16, &size)) {
        pkt_out->AddBlock(buff, size);
        return ErrorCode::None;
      } else {
        return ErrorCode::Failed;
      }
    }
    return ErrorCode::BadFormat;
  }

  // Read Wasm Memory.
  // IN : $qWasmMem:module_id;addr;len
  // OUT: $xx..xx
  if (toks[0] == "WasmMem") {
    if (toks.size() == 4) {
      uint32_t module_id = strtoul(toks[1].data(), nullptr, 10);
      uint32_t address = strtoul(toks[2].data(), nullptr, 16);
      uint32_t length = strtoul(toks[3].data(), nullptr, 16);
      if (length > Transport::kBufSize / 2) {
        return ErrorCode::BadArgs;
      }
      uint8_t buff[Transport::kBufSize];
      uint32_t read =
          gdb_server_->GetWasmMemory(module_id, address, buff, length);
      if (read > 0) {
        pkt_out->AddBlock(buff, read);
        return ErrorCode::None;
      } else {
        return ErrorCode::Failed;
      }
    }
    return ErrorCode::BadFormat;
  }

  // Read Wasm Data.
  // IN : $qWasmData:module_id;addr;len
  // OUT: $xx..xx
  if (toks[0] == "WasmData") {
    if (toks.size() == 4) {
      uint32_t module_id = strtoul(toks[1].data(), nullptr, 10);
      uint32_t address = strtoul(toks[2].data(), nullptr, 16);
      uint32_t length = strtoul(toks[3].data(), nullptr, 16);
      if (length > Transport::kBufSize / 2) {
        return ErrorCode::BadArgs;
      }
      uint8_t buff[Transport::kBufSize];
      uint32_t read =
          gdb_server_->GetWasmData(module_id, address, buff, length);
      if (read > 0) {
        pkt_out->AddBlock(buff, read);
        return ErrorCode::None;
      } else {
        return ErrorCode::Failed;
      }
    }
    return ErrorCode::BadFormat;
  }

  // No match so far, check the property cache.
  QueryPropertyMap::const_iterator it = query_properties_.find(toks[0]);
  if (it != query_properties_.end()) {
    pkt_out->AddString(it->second.data());
  }
  // If not found, just send an empty response.
  return ErrorCode::None;
}

// A Stop-reply packet has the format:
//   Sxx
// or:
//   Txx<name1>:<value1>;...;<nameN>:<valueN>
// where 'xx' is a two-digit hex number that represents the stop signal
// and the <name>:<value> pairs are used to report additional information,
// like the thread id.
void Target::SetStopReply(Packet* pkt_out) const {
  pkt_out->AddRawChar('T');
  pkt_out->AddWord8(cur_signal_);

  // Adds 'thread-pcs:<pc1>,...,<pcN>;' A list of pc values for all threads that
  // currently exist in the process.
  char buff[64];
  snprintf(buff, sizeof(buff), "thread-pcs:%" PRIx64 ";",
           static_cast<uint64_t>(GetCurrentPc()));
  pkt_out->AddString(buff);

  // Adds 'thread:<tid>;' pair. Note that a terminating ';' is required.
  pkt_out->AddString("thread:");
  pkt_out->AddNumberSep(kThreadId, ';');

  // If the loaded modules have changed since the last stop packet, signals
  // that.
  if (gdb_server_->HasModuleListChanged()) pkt_out->AddString("library:;");
}

void Target::SetStatus(Status status, int8_t signal,
                       std::vector<wasm_addr_t> call_frames, Isolate* isolate) {
  v8::base::MutexGuard guard(&mutex_);

  DCHECK((status == Status::Suspended && signal != 0 &&
          call_frames.size() > 0 && isolate != nullptr) ||
         (status != Status::Suspended && signal == 0 &&
          call_frames.size() == 0 && isolate == nullptr));

  current_isolate_ = isolate;
  status_ = status;
  cur_signal_ = signal;
  call_frames_ = call_frames;
}

const std::vector<wasm_addr_t> Target::GetCallStack() const {
  v8::base::MutexGuard guard(&mutex_);

  return call_frames_;
}

wasm_addr_t Target::GetCurrentPc() const {
  v8::base::MutexGuard guard(&mutex_);

  wasm_addr_t pc{0};
  if (call_frames_.size() > 0) {
    pc = call_frames_[0];
  }
  return pc;
}

}  // namespace gdb_server
}  // namespace wasm
}  // namespace internal
}  // namespace v8