text: // Copyright 2018 The Fuchsia 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/virtualization/tests/guest_console.h"
  
  #include <lib/syslog/cpp/macros.h>
  #include <lib/zx/clock.h>
  #include <lib/zx/time.h>
  #include <zircon/status.h>
  
  #include <iostream>
  
  #include "logger.h"
  #include "src/lib/fxl/strings/string_printf.h"
  
  static constexpr zx::duration kSerialStableDelay = zx::msec(800);
  
  // This is the maximum line length of dash in both zircon_guest and
  // debian_guest.
  static constexpr size_t kMaximumLineLength = 4096;
  
  static std::string command_hash(const std::string& command, uint64_t nonce) {
    return fxl::StringPrintf("%zu", std::hash<std::string>()(command) ^ std::hash<uint64_t>()(nonce));
  }
  
  static std::string normalize_new_lines(const std::string& s) {
    std::string result;
    // Strip carriage returns to normalise both guests on newlines only.
    for (const char c : s) {
      if (c != '\r') {
        result.push_back(c);
      }
    }
    return result;
  }
  
  GuestConsole::GuestConsole(std::unique_ptr<SocketInterface> socket) : socket_(std::move(socket)) {}
  
  zx_status_t GuestConsole::Start(zx::time deadline) {
    zx_status_t status;
  
    // Wait for something to be sent over serial. Both Zircon and Debian will send
    // at least a command prompt. For Debian, this is necessary since any commands
    // we send will be ignored until the guest is ready.
    status = WaitForAny(deadline);
    if (status != ZX_OK) {
      FX_LOGS(ERROR) << "Failed waiting for any output on the serial console: "
                     << zx_status_get_string(status);
      return status;
    }
  
    // Wait for output to stabilize.
    //
    // In particular, we wait for a duration of kSerialStableDelay to pass
    // without any output on the line before we consider the output stable.
    do {
      status = WaitForAny(zx::deadline_after(kSerialStableDelay));
      if (status != ZX_OK && status != ZX_ERR_TIMED_OUT) {
        FX_LOGS(ERROR) << "Failed waiting for serial console to stabilize: "
                       << zx_status_get_string(status);
        return status;
      }
  
      // If we've exceeded our deadline, abort.
      if (zx::clock::get_monotonic() >= deadline) {
        return ZX_ERR_TIMED_OUT;
      }
    } while (status == ZX_OK);
  
    return ZX_OK;
  }
  
  // Sends a command and waits for the response. We capture output by echoing a
  // header and footer before and after the command. Then we wait for the command
  // to be written back to the serial, then the header, then finally we capture
  // everything until the footer.
  zx_status_t GuestConsole::ExecuteBlocking(const std::string& command, const std::string& prompt,
                                            uint64_t nonce, zx::time deadline, std::string* result) {
    std::string header = command_hash(command, nonce);
    std::string footer = header;
    std::reverse(footer.begin(), footer.end());
  
    std::string full_command = "echo " + header + "; " + command + "; echo " + footer;
    if (full_command.size() > kMaximumLineLength) {
      FX_LOGS(ERROR) << "Command is too long";
      return ZX_ERR_OUT_OF_RANGE;
    }
  
    zx_status_t status = SendBlocking(full_command + "\n", deadline);
    if (status != ZX_OK) {
      FX_LOGS(INFO) << "Failed to send command: " << zx_status_get_string(status);
      return status;
    }
  
    std::string intermediate_result;
    status = WaitForMarker(full_command, deadline, &intermediate_result);
    if (status != ZX_OK) {
      FX_LOGS(INFO) << "Failed to wait for command echo: " << zx_status_get_string(status);
      FX_LOGS(INFO) << "Received: \"" << intermediate_result << "\"";
      return status;
    }
  
    status = WaitForMarker(header + "\n", deadline, &intermediate_result);
    if (status != ZX_OK) {
      FX_LOGS(INFO) << "Failed to wait for command header: " << zx_status_get_string(status);
      FX_LOGS(INFO) << "Received: \"" << intermediate_result << "\"";
      return status;
    }
  
    status = WaitForMarker(footer + "\n", deadline, result);
    if (status != ZX_OK) {
      FX_LOGS(INFO) << "Failed to wait for command footer: " << zx_status_get_string(status);
      if (result != nullptr && !result->empty()) {
        FX_LOGS(INFO) << "Received: \"" << *result << "\"";
      }
      return status;
    }
  
    status = WaitForMarker(prompt, deadline, &intermediate_result);
    if (status != ZX_OK) {
      FX_LOGS(INFO) << "Failed to wait for command prompt: " << zx_status_get_string(status);
      FX_LOGS(INFO) << "Received: \"" << intermediate_result << "\"";
      return status;
    }
  
    return ZX_OK;
  }
  
  zx_status_t GuestConsole::RepeatCommandTillSuccess(const std::string& command,
                                                     const std::string& prompt,
                                                     const std::string& success, zx::time deadline,
                                                     zx::duration repeat_rate) {
    uint64_t nonce = 0;
    do {
      std::string response;
      zx::time command_timeout = std::min(zx::deadline_after(repeat_rate), deadline);
      zx_status_t status = ExecuteBlocking(command, prompt, nonce, command_timeout, &response);
      if (status == ZX_OK && response.find(success) != std::string::npos) {
        return ZX_OK;
      }
  
      // In case the command failed early, wait till at least the repeat_rate deadline has passed
      // before trying agian.
      zx::nanosleep(std::min(command_timeout, deadline));
      ++nonce;
    } while (zx::clock::get_monotonic() < deadline);
  
    return ZX_ERR_TIMED_OUT;
  }
  
  zx_status_t GuestConsole::SendBlocking(const std::string& message, zx::time deadline) {
    return socket_->Send(deadline, message);
  }
  
  zx_status_t GuestConsole::WaitForMarker(const std::string& marker, zx::time deadline,
                                          std::string* result) {
    std::string output = buffer_;
    buffer_.erase();
    while (true) {
      // Check if the marker is already in our buffer.
      auto marker_loc = output.rfind(marker);
      if (marker_loc != std::string::npos) {
        // If we have read the socket past the end of the marker, make sure
        // what's left is kept in the buffer for the next read.
        if (marker_loc + marker.size() < output.size()) {
          buffer_ = output.substr(marker_loc + marker.size());
        }
        if (result == nullptr) {
          return ZX_OK;
        }
        output.erase(marker_loc);
        *result = output;
        return ZX_OK;
      }
  
      // Marker is not present: read some more data into the buffer.
      std::string buff;
      zx_status_t status = socket_->Receive(deadline, &buff);
      if (status != ZX_OK) {
        if (result != nullptr) {
          *result = output;
        }
        return status;
      }
      Logger::Get().Write(buff);
      output.append(normalize_new_lines(buff));
    }
  }
  
  zx_status_t GuestConsole::WaitForSocketClosed(zx::time deadline) {
    return socket_->WaitForClosed(deadline);
  }
  
  zx_status_t GuestConsole::Drain() {
    std::string result;
    zx_status_t status = DrainSocket(socket_.get(), &result);
    Logger::Get().Write(result);
    return status;
  }
  
  zx_status_t GuestConsole::WaitForAny(zx::time deadline) {
    std::string result;
    zx_status_t status = socket_->Receive(deadline, &result);
    if (status != ZX_OK) {
      return status;
    }
    Logger::Get().Write(result);
  
    Drain();
    return ZX_OK;
  }
  