/** @file

  traffic_ctl command for inspecting and clearing the cache shared-memory
  control segment and its associated stripe segments.

  The status and clear operations work by direct shm_open access rather than
  JSONRPC, so they function whether traffic_server is running or not. This
  is important for debugging crash-leftover segments when no live process
  is available to query.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#include "CacheShmCommand.h"
#include "shared/cache_shm/Layout.h"
#include "shared/cache_shm/Purge.h"
#include "TrafficCtlStatus.h"

#include "tscore/ink_config.h"

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <iostream>

namespace
{

// Must match the proxy.config.cache.shm.name_prefix default, or this tool inspects a different segment than the server made.
constexpr const char *DEFAULT_PREFIX = "ats";

#if TS_USE_CACHE_SHM

bool
shm_segment_exists(const std::string &name)
{
  int fd = shm_open(name.c_str(), O_RDONLY, 0);
  if (fd < 0) {
    return false;
  }
  close(fd);
  return true;
}

std::string
format_size(uint64_t bytes)
{
  char buf[64];
  if (bytes >= (uint64_t{1} << 30)) {
    std::snprintf(buf, sizeof(buf), "%.2f GiB", static_cast<double>(bytes) / (uint64_t{1} << 30));
  } else if (bytes >= (uint64_t{1} << 20)) {
    std::snprintf(buf, sizeof(buf), "%.2f MiB", static_cast<double>(bytes) / (uint64_t{1} << 20));
  } else if (bytes >= (uint64_t{1} << 10)) {
    std::snprintf(buf, sizeof(buf), "%.2f KiB", static_cast<double>(bytes) / (uint64_t{1} << 10));
  } else {
    std::snprintf(buf, sizeof(buf), "%llu B", static_cast<unsigned long long>(bytes));
  }
  return buf;
}

using cache_shm::process_is_alive;
using cache_shm::read_shm_name;

#endif // TS_USE_CACHE_SHM

} // namespace

CacheShmCommand::CacheShmCommand(ts::Arguments *args) : CtrlCommand(args)
{
  if (get_parsed_arguments()->get(STATUS_STR)) {
    _invoked_func = [this]() { status(); };
  } else if (get_parsed_arguments()->get(CLEAR_STR)) {
    _invoked_func = [this]() { clear(); };
  }
}

std::string
CacheShmCommand::get_prefix()
{
  // Framed the same way the server does, so the two agree on segment names.
  std::string configured = DEFAULT_PREFIX;
  if (auto arg = get_parsed_arguments()->get(PREFIX_STR); arg && !arg.empty()) {
    configured = arg.value();
  }
  return cache_shm::normalize_name_prefix(configured);
}

// A miss against the default prefix usually means name_prefix is set and was not repeated here; say so rather than leave
// the operator concluding shm was never enabled.
std::string
CacheShmCommand::default_prefix_hint()
{
  if (auto arg = get_parsed_arguments()->get(PREFIX_STR); arg && !arg.empty()) {
    return {};
  }
  return " (if proxy.config.cache.shm.name_prefix is set, pass --prefix <word>)";
}

void
CacheShmCommand::report_unsupported()
{
  std::cerr << "cache shm: this build has no POSIX shared memory support, so the cache shm "
               "fast-restart feature is compiled out; there is nothing to inspect or clear.\n";
  App_Exit_Status_Code = CTRL_EX_UNIMPLEMENTED;
}

void
CacheShmCommand::status()
{
#if !TS_USE_CACHE_SHM
  report_unsupported();
#else
  const std::string prefix       = get_prefix();
  const std::string control_name = cache_shm::control_segment_name(prefix);

  int fd = shm_open(control_name.c_str(), O_RDONLY, 0);
  if (fd < 0) {
    std::cerr << "cache shm: control segment '" << control_name << "' not found: " << std::strerror(errno) << default_prefix_hint()
              << '\n';
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  }

  struct stat sb {
  };
  if (fstat(fd, &sb) < 0) {
    std::cerr << "cache shm: fstat failed: " << std::strerror(errno) << '\n';
    close(fd);
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  }

  if (!cache_shm::is_own_control_size(static_cast<std::size_t>(sb.st_size))) {
    // stripes[] may have another stride, so interpreting it with our layout would print nonsense. `clear` refuses the
    // same segment for the same reason.
    std::cerr << "cache shm: control segment '" << control_name << "' is " << sb.st_size << " bytes, not this build's "
              << cache_shm::CONTROL_SIZE << "; it was written by a different build and cannot be interpreted.\n";
    close(fd);
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  }

  void *addr = mmap(nullptr, cache_shm::CONTROL_SIZE, PROT_READ, MAP_SHARED, fd, 0);
  close(fd);
  if (addr == MAP_FAILED) {
    std::cerr << "cache shm: mmap failed: " << std::strerror(errno) << '\n';
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  }

  const auto *ctrl = static_cast<const cache_shm::CacheShmControl *>(addr);

  const bool magic_ok  = std::memcmp(ctrl->magic, cache_shm::CACHE_SHM_MAGIC, sizeof(cache_shm::CACHE_SHM_MAGIC)) == 0;
  const bool schema_ok = ctrl->schema_version == cache_shm::CACHE_SHM_SCHEMA_VERSION;

  std::cout << "Control segment:    " << control_name << '\n';
  std::cout << "  segment size:     " << sb.st_size << " bytes (" << format_size(sb.st_size) << ")\n";
  std::cout << "  magic:            ";
  for (char c : ctrl->magic) {
    if (c >= 0x20 && c < 0x7f) {
      std::cout << c;
    }
  }
  std::cout << (magic_ok ? "  [valid]" : "  [INVALID]") << '\n';
  std::cout << "  schema_version:   " << ctrl->schema_version << (schema_ok ? "  [valid]" : "  [INVALID]") << '\n';
  std::cout << "  abi_hash:         0x" << std::hex << ctrl->abi_hash << std::dec << '\n';
  std::cout << "  storage_sig:      0x" << std::hex << ctrl->storage_signature << std::dec << '\n';
  std::cout << "  clean_shutdown:   " << static_cast<int>(ctrl->clean_shutdown)
            << (ctrl->clean_shutdown ? " (clean)" : " (DIRTY -- next start will rebuild)") << '\n';
  std::cout << "  owner_pid:        " << ctrl->owner_pid;
  if (ctrl->owner_pid == 0) {
    std::cout << " (none -- not currently attached)";
  } else if (process_is_alive(ctrl->owner_pid)) {
    std::cout << " (LIVE -- a running traffic_server owns this segment)";
  } else {
    std::cout << " (stale -- owner no longer running)";
  }
  std::cout << '\n';
  std::cout << "  stripe_count:     " << ctrl->stripe_count << '\n';

  if (!magic_ok || !schema_ok) {
    std::cout << "\nHeader is invalid; not interpreting stripe table.\n";
    munmap(addr, cache_shm::CONTROL_SIZE);
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  }

  const uint32_t shown = std::min<uint32_t>(ctrl->stripe_count, cache_shm::MAX_STRIPES);

  if (shown > 0) {
    std::cout << "\nStripes:\n";
    for (uint32_t i = 0; i < shown; ++i) {
      const auto &entry = ctrl->stripes[i];
      std::string name  = read_shm_name(entry.shm_name);
      if (name.empty()) {
        std::cout << "  [" << i << "] (tombstone -- slot free for reuse)\n";
        continue;
      }
      const bool present = shm_segment_exists(name);
      std::cout << "  [" << i << "] " << name << "  size=" << entry.raw_dir_size << " (" << format_size(entry.raw_dir_size) << ")  "
                << (present ? "present" : "MISSING") << (entry.dir_untrusted ? "  untrusted (will rebuild from disk)" : "") << '\n';
    }
  }

  if (ctrl->stripe_count > cache_shm::MAX_STRIPES) {
    std::cout << "\n(stripe_count " << ctrl->stripe_count << " exceeds MAX_STRIPES " << cache_shm::MAX_STRIPES << "; truncated.)\n";
  }

  munmap(addr, cache_shm::CONTROL_SIZE);
#endif // TS_USE_CACHE_SHM
}

void
CacheShmCommand::clear()
{
#if !TS_USE_CACHE_SHM
  report_unsupported();
#else
  // Shared with the server's purge-on-disabled-start; this only renders the result and sets the exit code.
  const cache_shm::PurgeReport report = cache_shm::purge_segments(get_prefix());

  switch (report.outcome) {
  case cache_shm::PurgeOutcome::BadPrefix:
    std::cerr << "cache shm: invalid prefix (must be non-empty and begin with '/').\n";
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  case cache_shm::PurgeOutcome::NotPresent:
    std::cerr << "cache shm: control segment '" << report.control_name << "' not found (" << std::strerror(report.sys_errno) << ")"
              << default_prefix_hint() << "; nothing to clear.\n";
    std::cout << "Removed 0 segment(s).\n";
    return;
  case cache_shm::PurgeOutcome::OpenFailed:
    // Not ENOENT: the segment may well exist but we could not open it (e.g. EACCES on a
    // segment owned by another user). Report the real errno and fail rather than claim success.
    std::cerr << "cache shm: cannot open control segment '" << report.control_name << "' (" << std::strerror(report.sys_errno)
              << "); cannot clear.\n";
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  case cache_shm::PurgeOutcome::MapFailed:
    std::cerr << "cache shm: mmap failed while reading stripe table: " << std::strerror(report.sys_errno) << '\n';
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  case cache_shm::PurgeOutcome::StatFailed:
    std::cerr << "cache shm: cannot stat control segment '" << report.control_name << "' (" << std::strerror(report.sys_errno)
              << "); cannot clear.\n";
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  case cache_shm::PurgeOutcome::OwnedByLive:
    // Refuse: unlinking a live owner's segments would orphan its fast restart.
    std::cerr << "cache shm: control segment '" << report.control_name << "' is owned by a live traffic_server (pid "
              << report.owner_pid << "); refusing to clear. Stop traffic_server first.\n";
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;
  case cache_shm::PurgeOutcome::TooSmall:
  case cache_shm::PurgeOutcome::Purged:
    break;
  }

  if (report.table_untrusted) {
    std::cerr << "cache shm: control segment '" << report.control_name << "' (" << report.segment_size
              << " bytes) was written by a different build; its stripe table could not be read, so every '" << get_prefix()
              << "s<N>' name was swept instead.\n";
  }

  for (const auto &u : report.unlinked) {
    if (u.error == 0) {
      std::cout << "unlinked " << u.name << '\n';
    } else if (u.error != ENOENT) {
      std::cerr << "failed to unlink " << u.name << ": " << std::strerror(u.error) << '\n';
    }
  }

  const unsigned failures = report.failures();
  std::cout << "Removed " << report.removed() << " segment(s)";
  if (failures != 0) {
    std::cout << ", " << failures << " failure(s)";
    App_Exit_Status_Code = CTRL_EX_ERROR;
  }
  std::cout << ".\n";
#endif // TS_USE_CACHE_SHM
}
