// Copyright 2019 Google LLC
//
// 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
//
//     https://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.

#ifndef SANDBOXED_API_TRANSACTION_H_
#define SANDBOXED_API_TRANSACTION_H_

#include <ctime>
#include <functional>
#include <memory>
#include <utility>

#include "absl/base/attributes.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/time/time.h"
#include "sandboxed_api/sandbox.h"

#define TRANSACTION_FAIL_IF_NOT(x, y)        \
  if (!(x)) {                                \
    return absl::FailedPreconditionError(y); \
  }

namespace sapi {

// The Transaction class allows to perform operations in the sandboxee,
// repeating them if necessary (if the sandboxing, or IPC failed).
//
// We provide two different implementations of transactions:
//  1) Single function transactions - They consist out of a single function
//     Main() that will be invoked as body of the transaction. For this,
//     inherit from the Transaction class and implement Main().
//  2) Function pointer based transactions - The BasicTransaction class accepts
//     functions that take a sandbox object (along with arbitrary other
//     parameters) and return a status. This way no custom implementation of a
//     Transaction class is required.
//
// Additionally both methods support Init() and Finish() functions.
// Init() will be called after the sandbox has been set up.
// Finish() will be called when the transaction object goes out of scope.
class TransactionBase {
 public:
  TransactionBase(const TransactionBase&) = delete;
  TransactionBase& operator=(const TransactionBase&) = delete;

  virtual ~TransactionBase();

  // Getter/Setter for retry_count_.
  int retry_count() const { return retry_count_; }
  void set_retry_count(int value) {
    CHECK_GE(value, 0);
    retry_count_ = value;
  }

  // Getter/Setter for time_limit_.
  time_t GetTimeLimit() const { return time_limit_; }
  void SetTimeLimit(time_t time_limit) { time_limit_ = time_limit; }
  void SetTimeLimit(absl::Duration time_limit) {
    time_limit_ = absl::ToTimeT(absl::UnixEpoch() + time_limit);
  }

  bool IsInitialized() const { return initialized_; }

  // Getter for the sandbox_.
  Sandbox* sandbox() const { return sandbox_.get(); }

  // Restarts the sandbox.
  // WARNING: This will invalidate any references to the remote process, make
  //          sure you don't keep any vars or FDs to the remote process when
  //          calling this.
  absl::Status Restart() {
    if (initialized_) {
      Finish().IgnoreError();
      initialized_ = false;
    }
    return sandbox_->Restart(true);
  }

 protected:
  explicit TransactionBase(std::unique_ptr<Sandbox> sandbox)
      : time_limit_(absl::ToTimeT(absl::UnixEpoch() + kDefaultTimeLimit)),
        sandbox_(std::move(sandbox)) {}

  // Runs the main (retrying) transaction loop.
  absl::Status RunTransactionLoop(const std::function<absl::Status()>& f);

 private:
  // Number of default transaction execution re-tries, in case of failures.
  static constexpr int kDefaultRetryCount = 1;

  // Wall-time limit for a single transaction execution (60 s.).
  static constexpr absl::Duration kDefaultTimeLimit = absl::Seconds(60);

  // Executes a single function in the sandbox, used in the main transaction
  // loop. Asserts that the sandbox has been set up and Init() was called.
  absl::Status RunTransactionFunctionInSandbox(
      const std::function<absl::Status()>& f);

  // Initialization routine of the sandboxed process that will be called only
  // once upon sandboxee startup.
  virtual absl::Status Init() { return absl::OkStatus(); }

  // End routine for the sandboxee that gets calls when the transaction is
  // destroyed/restarted to clean up resources.
  virtual absl::Status Finish() { return absl::OkStatus(); }

  // Number of tries this transaction will be re-executed until it succeeds.
  int retry_count_ = kDefaultRetryCount;

  // Time (wall-time) limit for a single Run() call (in seconds). 0 means: no
  // wall-time limit.
  time_t time_limit_;

  // Has Init() finished with success?
  bool initialized_ = false;

  // The main sapi::Sandbox object.
  std::unique_ptr<Sandbox> sandbox_;
};

// Regular style transactions, based on inheriting.
class Transaction : public TransactionBase {
 public:
  Transaction(const Transaction&) = delete;
  Transaction& operator=(const Transaction&) = delete;
  using TransactionBase::TransactionBase;

  // Run the transaction.
  absl::Status Run() {
    return RunTransactionLoop([this] { return Main(); });
  }

 protected:
  // The main sandboxee routine: Can be called multiple times.
  virtual absl::Status Main() { return absl::OkStatus(); }
};

// Callback style transactions:
class BasicTransaction final : public TransactionBase {
 private:
  using InitFunction = std::function<absl::Status(Sandbox*)>;
  using FinishFunction = std::function<absl::Status(Sandbox*)>;

 public:
  explicit BasicTransaction(std::unique_ptr<Sandbox> sandbox)
      : TransactionBase(std::move(sandbox)),
        init_function_(nullptr),
        finish_function_(nullptr) {}

  template <typename F>
  BasicTransaction(std::unique_ptr<Sandbox> sandbox, F init_function)
      : TransactionBase(std::move(sandbox)),
        init_function_(static_cast<InitFunction>(init_function)),
        finish_function_(nullptr) {}

  template <typename F, typename G>
  BasicTransaction(std::unique_ptr<Sandbox> sandbox, F init_function,
                   G fini_function)
      : TransactionBase(std::move(sandbox)),
        init_function_(static_cast<InitFunction>(init_function)),
        finish_function_(static_cast<FinishFunction>(fini_function)) {}

  // Run any function as body of the transaction that matches our expectations
  // (that is: Returning a Status and accepting a Sandbox object as first
  // parameter).
  template <typename T, typename... Args>
  absl::Status Run(T func, Args&&... args) {
    return RunTransactionLoop(
        [&] { return func(sandbox(), std::forward<Args>(args)...); });
  }

 private:
  InitFunction init_function_;
  FinishFunction finish_function_;

  absl::Status Init() final {
    return init_function_ ? init_function_(sandbox()) : absl::OkStatus();
  }

  absl::Status Finish() final {
    return finish_function_ ? finish_function_(sandbox()) : absl::OkStatus();
  }
};

}  // namespace sapi

#endif  // SANDBOXED_API_TRANSACTION_H_
