//===-- lldb-dap.cpp ------------------------------------------------------===//
//
// 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 "ClientLauncher.h"
#include "DAP.h"
#include "DAPLog.h"
#include "EventHelper.h"
#include "Handler/RequestHandler.h"
#include "Handler/ResponseHandler.h"
#include "RunInTerminal.h"
#include "Transport.h"
#include "lldb/API/SBDebugger.h"
#include "lldb/API/SBStream.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/File.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/MainLoop.h"
#include "lldb/Host/MainLoopBase.h"
#include "lldb/Host/MemoryMonitor.h"
#include "lldb/Host/Socket.h"
#include "lldb/Utility/AnsiTerminal.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/UriParser.h"
#include "lldb/lldb-forward.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/OptTable.h"
#include "llvm/Option/Option.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/PrettyStackTrace.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/WithColor.h"
#include "llvm/Support/raw_ostream.h"
#include <condition_variable>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>

#if defined(_WIN32)
// We need to #define NOMINMAX in order to skip `min()` and `max()` macro
// definitions that conflict with other system headers.
// We also need to #undef GetObject (which is defined to GetObjectW) because
// the JSON code we use also has methods named `GetObject()` and we conflict
// against these.
#define NOMINMAX
#include <windows.h>
#undef GetObject
#include <io.h>
typedef int socklen_t;
#else
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <termios.h>
#include <unistd.h>
#endif

#if defined(__linux__)
#include <sys/prctl.h>
#endif

using namespace lldb_dap;
using lldb_private::File;
using lldb_private::IOObject;
using lldb_private::MainLoop;
using lldb_private::MainLoopBase;
using lldb_private::NativeFile;
using lldb_private::Socket;
using lldb_private::Status;

namespace {
using namespace llvm::opt;

enum ID {
  OPT_INVALID = 0, // This is not an option ID.
#define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__),
#include "Options.inc"
#undef OPTION
};

#define OPTTABLE_STR_TABLE_CODE
#include "Options.inc"
#undef OPTTABLE_STR_TABLE_CODE

#define OPTTABLE_PREFIXES_TABLE_CODE
#include "Options.inc"
#undef OPTTABLE_PREFIXES_TABLE_CODE

static constexpr llvm::opt::OptTable::Info InfoTable[] = {
#define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),
#include "Options.inc"
#undef OPTION
};
class LLDBDAPOptTable : public llvm::opt::GenericOptTable {
public:
  LLDBDAPOptTable()
      : llvm::opt::GenericOptTable(OptionStrTable, OptionPrefixesTable,
                                   InfoTable, true) {}
};
} // anonymous namespace

static void PrintHelp(LLDBDAPOptTable &table, llvm::StringRef tool_name) {
  std::string usage_str = tool_name.str() + " options";
  table.printHelp(llvm::outs(), usage_str.c_str(), "LLDB DAP", false);

  llvm::outs() << R"___(
EXAMPLES:
  The debug adapter can be started in two modes.

  Running lldb-dap without any arguments will start communicating with the
  parent over stdio. Passing a --connection URI will cause lldb-dap to listen
  for a connection in the specified mode.

    lldb-dap --connection listen://localhost:<port>

  Passing --wait-for-debugger will pause the process at startup and wait for a
  debugger to attach to the process.

    lldb-dap -g

  You can also use lldb-dap to launch a supported client, for example the
  LLDB-DAP Visual Studio Code extension.

    lldb-dap --client vscode -- /path/to/binary <args>

)___";
}

static void PrintVersion() {
  llvm::outs() << "lldb-dap: ";
  llvm::cl::PrintVersionMessage();
  llvm::outs() << "liblldb: " << lldb::SBDebugger::GetVersionString() << '\n';
}

static llvm::Error LaunchClient(const llvm::opt::InputArgList &args) {
  auto *client_arg = args.getLastArg(OPT_client);
  assert(client_arg && "must have client arg");

  std::optional<ClientLauncher::Client> client =
      ClientLauncher::GetClientFrom(client_arg->getValue());
  if (!client)
    return llvm::createStringError(
        llvm::formatv("unsupported client: {0}", client_arg->getValue()));

  std::vector<llvm::StringRef> launch_args;
  if (auto *arg = args.getLastArgNoClaim(OPT_REM)) {
    for (auto *value : arg->getValues()) {
      launch_args.push_back(value);
    }
  }

  if (launch_args.empty())
    return llvm::createStringError("no launch arguments provided");

  return ClientLauncher::GetLauncher(*client)->Launch(launch_args);
}

#if not defined(_WIN32)
struct FDGroup {
  int GetFlags() const {
    if (read && write)
      return O_NOCTTY | O_CREAT | O_RDWR;
    if (read)
      return O_NOCTTY | O_RDONLY;
    return O_NOCTTY | O_CREAT | O_WRONLY | O_TRUNC;
  }

  std::vector<int> fds;
  bool read = false;
  bool write = false;
};

static llvm::Error RedirectToFile(const FDGroup &fdg, llvm::StringRef file) {
  if (!fdg.read && !fdg.write)
    return llvm::Error::success();
  int target_fd = lldb_private::FileSystem::Instance().Open(
      file.str().c_str(), fdg.GetFlags(), 0666);
  if (target_fd == -1)
    return llvm::errorCodeToError(
        std::error_code(errno, std::generic_category()));
  for (int fd : fdg.fds) {
    if (target_fd == fd)
      continue;
    if (::dup2(target_fd, fd) == -1)
      return llvm::errorCodeToError(
          std::error_code(errno, std::generic_category()));
  }
  ::close(target_fd);
  return llvm::Error::success();
}

static llvm::Error
SetupIORedirection(const llvm::SmallVectorImpl<llvm::StringRef> &files) {
  llvm::SmallDenseMap<llvm::StringRef, FDGroup> groups;
  for (size_t i = 0; i < files.size(); i++) {
    if (files[i].empty())
      continue;
    auto group = groups.find(files[i]);
    if (group == groups.end())
      group = groups.insert({files[i], {{static_cast<int>(i)}}}).first;
    else
      group->second.fds.push_back(i);
    switch (i) {
    case 0:
      group->second.read = true;
      break;
    case 1:
    case 2:
      group->second.write = true;
      break;
    default:
      group->second.read = true;
      group->second.write = true;
      break;
    }
  }
  for (const auto &[file, group] : groups) {
    if (llvm::Error err = RedirectToFile(group, file))
      return llvm::createStringError(
          llvm::formatv("{0}: {1}", file, llvm::toString(std::move(err))));
  }
  return llvm::Error::success();
}
#endif

// If --launch-target is provided, this instance of lldb-dap becomes a
// runInTerminal launcher. It will ultimately launch the program specified in
// the --launch-target argument, which is the original program the user wanted
// to debug. This is done in such a way that the actual debug adapter can
// place breakpoints at the beginning of the program.
//
// The launcher will communicate with the debug adapter using a fifo file in the
// directory specified in the --comm-file argument.
//
// Regarding the actual flow, this launcher will first notify the debug adapter
// of its pid. Then, the launcher will be in a pending state waiting to be
// attached by the adapter.
//
// Once attached and resumed, the launcher will exec and become the program
// specified by --launch-target, which is the original target the
// user wanted to run.
//
// In case of errors launching the target, a suitable error message will be
// emitted to the debug adapter.
static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg,
                                             llvm::StringRef comm_file,
                                             lldb::pid_t debugger_pid,
                                             llvm::StringRef stdio,
                                             char *argv[]) {
#if defined(_WIN32)
  return llvm::createStringError(
      "runInTerminal is only supported on POSIX systems");
#else

  // On Linux with the Yama security module enabled, a process can only attach
  // to its descendants by default. In the runInTerminal case the target
  // process is launched by the client so we need to allow tracing explicitly.
#if defined(__linux__)
  if (debugger_pid != LLDB_INVALID_PROCESS_ID)
    (void)prctl(PR_SET_PTRACER, debugger_pid, 0, 0, 0);
#endif

  lldb_private::FileSystem::Initialize();
  if (!stdio.empty()) {
    llvm::SmallVector<llvm::StringRef, 3> files;
    stdio.split(files, ':');
    while (files.size() < 3)
      files.push_back(files.back());
    if (llvm::Error err = SetupIORedirection(files))
      return err;
  } else if ((isatty(STDIN_FILENO) != 0) &&
             llvm::StringRef(getenv("TERM")).starts_with_insensitive("xterm")) {
    // Clear the screen.
    llvm::outs() << ANSI_CSI_RESET_CURSOR ANSI_CSI_ERASE_VIEWPORT
            ANSI_CSI_ERASE_SCROLLBACK;
    // VS Code will reuse the same terminal for the same debug configuration
    // between runs. Clear the input buffer prior to starting the new process so
    // prior input is not carried forward to the new debug session.
    tcflush(STDIN_FILENO, TCIFLUSH);
  }

  RunInTerminalLauncherCommChannel comm_channel(comm_file);
  if (llvm::Error err = comm_channel.NotifyPid())
    return err;

  // We will wait to be attached with a timeout. We don't wait indefinitely
  // using a signal to prevent being paused forever.

  // This env var should be used only for tests.
  const char *timeout_env_var = getenv("LLDB_DAP_RIT_TIMEOUT_IN_MS");
  int timeout_in_ms =
      timeout_env_var != nullptr ? atoi(timeout_env_var) : 20000;
  if (llvm::Error err = comm_channel.WaitUntilDebugAdapterAttaches(
          std::chrono::milliseconds(timeout_in_ms))) {
    return err;
  }

  const char *target = target_arg.getValue();
  execvp(target, argv);

  std::string error = std::strerror(errno);
  comm_channel.NotifyError(error);
  return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                 std::move(error));
#endif
}

/// used only by TestVSCode_redirection_to_console.py
static void redirection_test() {
  printf("stdout message\n");
  fprintf(stderr, "stderr message\n");
  fflush(stdout);
  fflush(stderr);
}

/// Duplicates a file descriptor, setting FD_CLOEXEC if applicable.
static int DuplicateFileDescriptor(int fd) {
#if defined(F_DUPFD_CLOEXEC)
  // Ensure FD_CLOEXEC is set.
  return ::fcntl(fd, F_DUPFD_CLOEXEC, 0);
#else
  return ::dup(fd);
#endif
}

static void
ResetConnectionTimeout(std::mutex &connection_timeout_mutex,
                       MainLoopBase::TimePoint &conncetion_timeout_time_point) {
  std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
  conncetion_timeout_time_point = MainLoopBase::TimePoint();
}

static void
TrackConnectionTimeout(MainLoop &loop, std::mutex &connection_timeout_mutex,
                       MainLoopBase::TimePoint &conncetion_timeout_time_point,
                       std::chrono::seconds ttl_seconds) {
  MainLoopBase::TimePoint next_checkpoint =
      std::chrono::steady_clock::now() + std::chrono::seconds(ttl_seconds);
  {
    std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
    // We don't need to take the max of `ttl_time_point` and `next_checkpoint`,
    // because `next_checkpoint` must be the latest.
    conncetion_timeout_time_point = next_checkpoint;
  }
  loop.AddCallback(
      [&connection_timeout_mutex, &conncetion_timeout_time_point,
       next_checkpoint](MainLoopBase &loop) {
        std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
        if (conncetion_timeout_time_point == next_checkpoint)
          loop.RequestTermination();
      },
      next_checkpoint);
}

static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>>
validateConnection(llvm::StringRef conn) {
  auto uri = lldb_private::URI::Parse(conn);

  auto make_error = [conn]() -> llvm::Error {
    return llvm::createStringError(
        "Unsupported connection specifier, expected 'accept:///path' or "
        "'listen://[host]:port', got '%s'.",
        conn.str().c_str());
  };

  if (!uri)
    return make_error();

  std::optional<Socket::ProtocolModePair> protocol_and_mode =
      Socket::GetProtocolAndMode(uri->scheme);
  if (!protocol_and_mode || protocol_and_mode->second != Socket::ModeAccept)
    return make_error();

  if (protocol_and_mode->first == Socket::ProtocolTcp) {
    return std::make_pair(
        Socket::ProtocolTcp,
        formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname,
                uri->port.value_or(0)));
  }

  if (protocol_and_mode->first == Socket::ProtocolUnixDomain)
    return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str());

  return make_error();
}

static llvm::Error serveConnection(
    const Socket::SocketProtocol &protocol, llvm::StringRef name, Log &log,
    const ReplMode default_repl_mode,
    const std::vector<std::string> &pre_init_commands, bool no_lldbinit,
    std::optional<std::chrono::seconds> connection_timeout_seconds) {
  Status status;
  static std::unique_ptr<Socket> listener = Socket::Create(protocol, status);
  if (status.Fail()) {
    return status.takeError();
  }

  status = listener->Listen(name, /*backlog=*/5);
  if (status.Fail()) {
    return status.takeError();
  }

  std::string address = llvm::join(listener->GetListeningConnectionURI(), ", ");
  DAP_LOG(log, "started with connection listeners {0}", address);

  llvm::outs() << "Listening for: " << address << "\n";
  // Ensure listening address are flushed for calles to retrieve the resolve
  // address.
  llvm::outs().flush();

  static MainLoop g_loop;
  llvm::sys::SetInterruptFunction([]() {
    g_loop.AddPendingCallback(
        [](MainLoopBase &loop) { loop.RequestTermination(); });
  });
  static MainLoopBase::TimePoint g_connection_timeout_time_point;
  static std::mutex g_connection_timeout_mutex;
  if (connection_timeout_seconds)
    TrackConnectionTimeout(g_loop, g_connection_timeout_mutex,
                           g_connection_timeout_time_point,
                           connection_timeout_seconds.value());
  unsigned int clientCount = 0;
  auto handle = listener->Accept(g_loop, [=, &log, &clientCount](
                                             std::unique_ptr<Socket> sock) {
    // Reset the keep alive timer, because we won't be killing the server
    // while this connection is being served.
    if (connection_timeout_seconds)
      ResetConnectionTimeout(g_connection_timeout_mutex,
                             g_connection_timeout_time_point);
    const std::string client_name = llvm::formatv("conn{0}", clientCount++);
    lldb::IOObjectSP io(std::move(sock));

    // Move the client into a background thread to unblock accepting the next
    // client.
    std::thread client([=, &log]() {
      llvm::set_thread_name(client_name + ".runloop");

      Log client_log = log.WithPrefix("(" + client_name + ")");
      DAP_LOG(client_log, "client connected");

      MainLoop loop;
      Transport transport(client_log, io, io);
      DAP dap(client_log, default_repl_mode, pre_init_commands, no_lldbinit,
              client_name, transport, loop);

      if (auto Err = dap.ConfigureIO()) {
        llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                    "Failed to configure stdout redirect: ");
        return;
      }

      // Register the DAP session with the global manager.
      DAPSessionManager::GetInstance().RegisterSession(&loop, &dap);

      if (auto Err = dap.Loop()) {
        llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                    "DAP session (" + client_name +
                                        ") error: ");
      }

      DAP_LOG(client_log, "client disconnected");
      // Unregister the DAP session from the global manager.
      DAPSessionManager::GetInstance().UnregisterSession(&loop);
      // Start the countdown to kill the server at the end of each connection.
      if (connection_timeout_seconds)
        TrackConnectionTimeout(g_loop, g_connection_timeout_mutex,
                               g_connection_timeout_time_point,
                               connection_timeout_seconds.value());
    });
    client.detach();
  });

  if (auto Err = handle.takeError()) {
    return Err;
  }

  status = g_loop.Run();
  if (status.Fail()) {
    return status.takeError();
  }

  DAP_LOG(log, "server shutting down, disconnecting remaining clients");

  // Disconnect all active sessions using the global manager.
  DAPSessionManager::GetInstance().DisconnectAllSessions();

  // Wait for all clients to finish disconnecting and return any errors.
  return DAPSessionManager::GetInstance().WaitForAllSessionsToDisconnect();
}

int main(int argc, char *argv[]) {
  llvm::InitLLVM IL(argc, argv, /*InstallPipeSignalExitHandler=*/false);
#if !defined(__APPLE__)
  llvm::setBugReportMsg("PLEASE submit a bug report to " LLDB_BUG_REPORT_URL
                        " and include the crash backtrace.\n");
#else
  llvm::setBugReportMsg("PLEASE submit a bug report to " LLDB_BUG_REPORT_URL
                        " and include the crash report from "
                        "~/Library/Logs/DiagnosticReports/.\n");
#endif

  llvm::SmallString<256> program_path(argv[0]);
  llvm::sys::fs::make_absolute(program_path);
  DAP::debug_adapter_path = program_path;

  LLDBDAPOptTable T;
  unsigned MAI, MAC;
  llvm::ArrayRef<const char *> ArgsArr = llvm::ArrayRef(argv + 1, argc);
  llvm::opt::InputArgList input_args = T.ParseArgs(ArgsArr, MAI, MAC);

  if (input_args.hasArg(OPT_help)) {
    PrintHelp(T, llvm::sys::path::filename(argv[0]));
    return EXIT_SUCCESS;
  }

  if (input_args.hasArg(OPT_version)) {
    PrintVersion();
    return EXIT_SUCCESS;
  }

  if (input_args.hasArg(OPT_client)) {
    if (llvm::Error error = LaunchClient(input_args)) {
      llvm::WithColor::error() << llvm::toString(std::move(error)) << '\n';
      return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
  }

  ReplMode default_repl_mode = ReplMode::Auto;
  if (input_args.hasArg(OPT_repl_mode)) {
    llvm::opt::Arg *repl_mode = input_args.getLastArg(OPT_repl_mode);
    llvm::StringRef repl_mode_value = repl_mode->getValue();
    if (repl_mode_value == "auto") {
      default_repl_mode = ReplMode::Auto;
    } else if (repl_mode_value == "variable") {
      default_repl_mode = ReplMode::Variable;
    } else if (repl_mode_value == "command") {
      default_repl_mode = ReplMode::Command;
    } else {
      llvm::errs() << "'" << repl_mode_value
                   << "' is not a valid option, use 'variable', 'command' or "
                      "'auto'.\n";
      return EXIT_FAILURE;
    }
  }

  if (llvm::opt::Arg *target_arg = input_args.getLastArg(OPT_launch_target)) {
    if (llvm::opt::Arg *comm_file = input_args.getLastArg(OPT_comm_file)) {
      lldb::pid_t pid = LLDB_INVALID_PROCESS_ID;
      llvm::opt::Arg *debugger_pid = input_args.getLastArg(OPT_debugger_pid);
      if (debugger_pid) {
        llvm::StringRef debugger_pid_value = debugger_pid->getValue();
        if (debugger_pid_value.getAsInteger(10, pid)) {
          llvm::errs() << "'" << debugger_pid_value
                       << "' is not a valid "
                          "PID\n";
          return EXIT_FAILURE;
        }
      }
      int target_args_pos = argc;
      for (int i = 0; i < argc; i++) {
        if (strcmp(argv[i], "--launch-target") == 0) {
          target_args_pos = i + 1;
          break;
        }
      }
      llvm::StringRef stdio = input_args.getLastArgValue(OPT_stdio);
      if (llvm::Error err =
              LaunchRunInTerminalTarget(*target_arg, comm_file->getValue(), pid,
                                        stdio, argv + target_args_pos)) {
        llvm::errs() << llvm::toString(std::move(err)) << '\n';
        return EXIT_FAILURE;
      }
    } else {
      llvm::errs() << "\"--launch-target\" requires \"--comm-file\" to be "
                      "specified\n";
      return EXIT_FAILURE;
    }
  }

  std::string connection;
  if (auto *arg = input_args.getLastArg(OPT_connection)) {
    const auto *path = arg->getValue();
    connection.assign(path);
  }

  std::optional<std::chrono::seconds> connection_timeout_seconds;
  if (llvm::opt::Arg *connection_timeout_arg =
          input_args.getLastArg(OPT_connection_timeout)) {
    if (!connection.empty()) {
      llvm::StringRef connection_timeout_string_value =
          connection_timeout_arg->getValue();
      int connection_timeout_int_value;
      if (connection_timeout_string_value.getAsInteger(
              10, connection_timeout_int_value)) {
        llvm::errs() << "'" << connection_timeout_string_value
                     << "' is not a valid connection timeout value\n";
        return EXIT_FAILURE;
      }
      // Ignore non-positive values.
      if (connection_timeout_int_value > 0)
        connection_timeout_seconds =
            std::chrono::seconds(connection_timeout_int_value);
    } else {
      llvm::errs()
          << "\"--connection-timeout\" requires \"--connection\" to be "
             "specified\n";
      return EXIT_FAILURE;
    }
  }

#if !defined(_WIN32)
  if (input_args.hasArg(OPT_wait_for_debugger)) {
    printf("Paused waiting for debugger to attach (pid = %i)...\n", getpid());
    pause();
  }
#endif

  std::unique_ptr<llvm::raw_ostream> log_os;
  if (const char *log_file_path = getenv("LLDBDAP_LOG"); log_file_path) {
    int FD;
    if (std::error_code EC =
            llvm::sys::fs::openFileForWrite(log_file_path, FD)) {
      llvm::errs() << "Failed to open log file: " << log_file_path << ": "
                   << EC.message() << "\n";
      return EXIT_FAILURE;
    }
    log_os = std::make_unique<llvm::raw_fd_ostream>(FD, /*shouldClose=*/true);
  }
  Log::Mutex mutex;
  Log log(log_os ? *log_os : llvm::nulls(), mutex);

  // Initialize LLDB first before we do anything.
  lldb::SBError error = lldb::SBDebugger::InitializeWithErrorHandling();
  if (error.Fail()) {
    lldb::SBStream os;
    error.GetDescription(os);
    llvm::errs() << "lldb initialize failed: " << os.GetData() << "\n";
    return EXIT_FAILURE;
  }

  // Create a memory monitor. This can return nullptr if the host platform is
  // not supported.
  std::unique_ptr<lldb_private::MemoryMonitor> memory_monitor =
      lldb_private::MemoryMonitor::Create([&log]() {
        DAP_LOG(log, "memory pressure detected");
        lldb::SBDebugger::MemoryPressureDetected();
      });

  if (memory_monitor)
    memory_monitor->Start();

  // Terminate the debugger before the C++ destructor chain kicks in.
  llvm::scope_exit terminate_debugger([&] {
    if (memory_monitor)
      memory_monitor->Stop();
    lldb::SBDebugger::Terminate();
  });

  std::vector<std::string> pre_init_commands;
  for (const std::string &arg :
       input_args.getAllArgValues(OPT_pre_init_command)) {
    pre_init_commands.push_back(arg);
  }

  bool no_lldbinit = input_args.hasArg(OPT_no_lldbinit);

  if (!connection.empty()) {
    auto maybeProtoclAndName = validateConnection(connection);
    if (auto Err = maybeProtoclAndName.takeError()) {
      llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                  "Invalid connection: ");
      return EXIT_FAILURE;
    }

    Socket::SocketProtocol protocol;
    std::string name;
    std::tie(protocol, name) = *maybeProtoclAndName;
    if (auto Err = serveConnection(protocol, name, log, default_repl_mode,
                                   pre_init_commands, no_lldbinit,
                                   connection_timeout_seconds)) {
      llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                  "Connection failed: ");
      return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
  }

#if defined(_WIN32)
  // Windows opens stdout and stdin in text mode which converts \n to 13,10
  // while the value is just 10 on Darwin/Linux. Setting the file mode to
  // binary fixes this.
  int result = _setmode(fileno(stdout), _O_BINARY);
  assert(result);
  result = _setmode(fileno(stdin), _O_BINARY);
  UNUSED_IF_ASSERT_DISABLED(result);
  assert(result);
#endif

  int stdout_fd = DuplicateFileDescriptor(fileno(stdout));
  if (stdout_fd == -1) {
    llvm::logAllUnhandledErrors(
        llvm::errorCodeToError(llvm::errnoAsErrorCode()), llvm::errs(),
        "Failed to configure stdout redirect: ");
    return EXIT_FAILURE;
  }

  lldb::IOObjectSP input = std::make_shared<NativeFile>(
      fileno(stdin), File::eOpenOptionReadOnly, NativeFile::Unowned);
  lldb::IOObjectSP output = std::make_shared<NativeFile>(
      stdout_fd, File::eOpenOptionWriteOnly, NativeFile::Unowned);

  constexpr llvm::StringLiteral client_name = "stdio";
  MainLoop loop;
  Log client_log = log.WithPrefix("(stdio)");
  Transport transport(client_log, input, output);
  DAP dap(client_log, default_repl_mode, pre_init_commands, no_lldbinit,
          client_name, transport, loop);

  // stdout/stderr redirection to the IDE's console
  if (auto Err = dap.ConfigureIO(stdout, stderr)) {
    llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                "Failed to configure stdout redirect: ");
    return EXIT_FAILURE;
  }

  // Register the DAP session with the global manager for stdio mode.
  // This is needed for the event handling to find the correct DAP instance.
  DAPSessionManager::GetInstance().RegisterSession(&loop, &dap);

  // used only by TestVSCode_redirection_to_console.py
  if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr)
    redirection_test();

  if (auto Err = dap.Loop()) {
    DAP_LOG(client_log, "DAP session error: {0}",
            llvm::toStringWithoutConsuming(Err));
    llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
                                "DAP session error: ");
    DAPSessionManager::GetInstance().UnregisterSession(&loop);
    return EXIT_FAILURE;
  }
  DAPSessionManager::GetInstance().UnregisterSession(&loop);
  return EXIT_SUCCESS;
}
