/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed 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 "src/perfetto_cmd/rate_limiter.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <algorithm>
#include <cinttypes>

#include "perfetto/base/logging.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/utils.h"
#include "src/perfetto_cmd/perfetto_cmd.h"

namespace perfetto {
namespace {

// Every 24 hours we reset how much we've uploaded.
const uint64_t kMaxUploadResetPeriodInSeconds = 60 * 60 * 24;

// Maximum of 10mb every 24h.
const uint64_t kMaxUploadInBytes = 1024 * 1024 * 10;

// Keep track of last 10 observed sessions.
const uint64_t kMaxSessionsInHistory = 10;

}  // namespace

using PerSessionState = gen::PerfettoCmdState::PerSessionState;

RateLimiter::RateLimiter() = default;
RateLimiter::~RateLimiter() = default;

RateLimiter::ShouldTraceResponse RateLimiter::ShouldTrace(const Args& args) {
  uint64_t now_in_s = static_cast<uint64_t>(args.current_time.count());

  // Not uploading?
  // -> We can just trace.
  if (!args.is_uploading)
    return ShouldTraceResponse::kOkToTrace;

  // If we're tracing a user build we should only trace if the override in
  // the config is set:
  if (args.is_user_build && !args.allow_user_build_tracing) {
    PERFETTO_ELOG(
        "Guardrail: allow_user_build_tracing must be set to trace on user "
        "builds");
    return ShouldTraceResponse::kNotAllowedOnUserBuild;
  }

  // The state file is gone.
  // Maybe we're tracing for the first time or maybe something went wrong the
  // last time we tried to save the state. Either way reinitialize the state
  // file.
  if (!StateFileExists()) {
    // We can't write the empty state file?
    // -> Give up.
    if (!ClearState()) {
      PERFETTO_ELOG("Guardrail: failed to initialize guardrail state.");
      return ShouldTraceResponse::kFailedToInitState;
    }
  }

  bool loaded_state = LoadState(&state_);

  // Failed to load the state?
  // Current time is before either saved times?
  // Last saved trace time is before first saved trace time?
  // -> Try to save a clean state but don't trace.
  if (!loaded_state || now_in_s < state_.first_trace_timestamp() ||
      now_in_s < state_.last_trace_timestamp() ||
      state_.last_trace_timestamp() < state_.first_trace_timestamp()) {
    ClearState();
    PERFETTO_ELOG("Guardrail: state invalid, clearing it.");
    if (!args.ignore_guardrails)
      return ShouldTraceResponse::kInvalidState;
  }

  // First trace was more than 24h ago? Reset state.
  if ((now_in_s - state_.first_trace_timestamp()) >
      kMaxUploadResetPeriodInSeconds) {
    state_.set_first_trace_timestamp(0);
    state_.set_last_trace_timestamp(0);
    state_.set_total_bytes_uploaded(0);
    return ShouldTraceResponse::kOkToTrace;
  }

  uint64_t max_upload_guardrail = kMaxUploadInBytes;
  if (args.max_upload_bytes_override > 0) {
    if (args.unique_session_name.empty()) {
      PERFETTO_ELOG(
          "Ignoring max_upload_per_day_bytes override as unique_session_name "
          "not set");
    } else {
      max_upload_guardrail = args.max_upload_bytes_override;
    }
  }

  uint64_t uploaded_so_far = state_.total_bytes_uploaded();
  if (!args.unique_session_name.empty()) {
    uploaded_so_far = 0;
    for (const auto& session_state : state_.session_state()) {
      if (session_state.session_name() == args.unique_session_name) {
        uploaded_so_far = session_state.total_bytes_uploaded();
        break;
      }
    }
  }

  if (uploaded_so_far > max_upload_guardrail) {
    PERFETTO_ELOG("Guardrail: Uploaded %" PRIu64
                  " in the last 24h. Limit is %" PRIu64 ".",
                  uploaded_so_far, max_upload_guardrail);
    if (!args.ignore_guardrails)
      return ShouldTraceResponse::kHitUploadLimit;
  }

  return ShouldTraceResponse::kOkToTrace;
}

bool RateLimiter::OnTraceDone(const Args& args, bool success, uint64_t bytes) {
  uint64_t now_in_s = static_cast<uint64_t>(args.current_time.count());

  // Failed to upload? Don't update the state.
  if (!success)
    return false;

  if (!args.is_uploading)
    return true;

  // If the first trace timestamp is 0 (either because this is the
  // first time or because it was reset for being more than 24h ago).
  // -> We update it to the time of this trace.
  if (state_.first_trace_timestamp() == 0)
    state_.set_first_trace_timestamp(now_in_s);
  // Always updated the last trace timestamp.
  state_.set_last_trace_timestamp(now_in_s);

  if (args.unique_session_name.empty()) {
    // Add the amount we uploaded to the running total.
    state_.set_total_bytes_uploaded(state_.total_bytes_uploaded() + bytes);
  } else {
    PerSessionState* target_session = nullptr;
    for (PerSessionState& session : *state_.mutable_session_state()) {
      if (session.session_name() == args.unique_session_name) {
        target_session = &session;
        break;
      }
    }
    if (!target_session) {
      target_session = state_.add_session_state();
      target_session->set_session_name(args.unique_session_name);
    }
    target_session->set_total_bytes_uploaded(
        target_session->total_bytes_uploaded() + bytes);
    target_session->set_last_trace_timestamp(now_in_s);
  }

  if (!SaveState(state_)) {
    PERFETTO_ELOG("Failed to save state.");
    return false;
  }

  return true;
}

std::string RateLimiter::GetStateFilePath() const {
  return std::string(kStateDir) + "/.guardraildata";
}

bool RateLimiter::StateFileExists() {
  struct stat out;
  return stat(GetStateFilePath().c_str(), &out) != -1;
}

bool RateLimiter::ClearState() {
  gen::PerfettoCmdState zero{};
  zero.set_total_bytes_uploaded(0);
  zero.set_last_trace_timestamp(0);
  zero.set_first_trace_timestamp(0);
  bool success = SaveState(zero);
  if (!success && StateFileExists())
    remove(GetStateFilePath().c_str());
  return success;
}

bool RateLimiter::LoadState(gen::PerfettoCmdState* state) {
  base::ScopedFile in_fd(base::OpenFile(GetStateFilePath(), O_RDONLY));
  if (!in_fd)
    return false;
  std::string s;
  base::ReadFileDescriptor(in_fd.get(), &s);
  if (s.empty())
    return false;
  return state->ParseFromString(s);
}

bool RateLimiter::SaveState(const gen::PerfettoCmdState& input_state) {
  gen::PerfettoCmdState state = input_state;

  // Keep only the N most recent per session states so the file doesn't
  // grow indefinitely:
  std::vector<PerSessionState>* sessions = state.mutable_session_state();
  std::sort(sessions->begin(), sessions->end(),
            [](const PerSessionState& a, const PerSessionState& b) {
              return a.last_trace_timestamp() > b.last_trace_timestamp();
            });
  if (sessions->size() > kMaxSessionsInHistory) {
    sessions->resize(kMaxSessionsInHistory);
  }

  // Rationale for 0666: the cmdline client can be executed under two
  // different Unix UIDs: shell and statsd. If we run one after the
  // other and the file has 0600 permissions, then the 2nd run won't
  // be able to read the file and will clear it, aborting the trace.
  // SELinux still prevents that anything other than the perfetto
  // executable can change the guardrail file.
  std::vector<uint8_t> buf = state.SerializeAsArray();
  base::ScopedFile out_fd(
      base::OpenFile(GetStateFilePath(), O_WRONLY | O_CREAT | O_TRUNC, 0666));
  if (!out_fd)
    return false;
  ssize_t written = base::WriteAll(out_fd.get(), buf.data(), buf.size());
  return written >= 0 && static_cast<size_t>(written) == buf.size();
}

}  // namespace perfetto
