//
// Copyright (C) 2020 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.
//

// update_engine console client installed to APEXes for scripts to invoke
// directly. Uses the stable API.

#include <fcntl.h>
#include <sysexits.h>
#include <unistd.h>

#include <vector>

#include <aidl/android/os/BnUpdateEngineStableCallback.h>
#include <aidl/android/os/IUpdateEngineStable.h>
#include <android-base/logging.h>
#include <android-base/strings.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
#include <android/binder_ibinder.h>
#include <common/error_code.h>
#include <gflags/gflags.h>

namespace chromeos_update_engine::internal {

DEFINE_string(payload,
              "file:///path/to/payload.bin",
              "The file URI to the update payload to use, or path to the file");
DEFINE_int64(offset,
             0,
             "The offset in the payload where the CrAU update starts.");
DEFINE_int64(size,
             0,
             "The size of the CrAU part of the payload. If 0 is passed, it "
             "will be autodetected.");
DEFINE_string(headers,
              "",
              "A list of key-value pairs, one element of the list per line.");

[[noreturn]] int Exit(int return_code) {
  LOG(INFO) << "Exit: " << return_code;
  exit(return_code);
}
// Called whenever the UpdateEngine daemon dies.
void UpdateEngineServiceDied(void*) {
  LOG(ERROR) << "UpdateEngineService died.";
  Exit(EX_SOFTWARE);
}

class UpdateEngineClientAndroid {
 public:
  UpdateEngineClientAndroid() = default;
  int Run();

 private:
  class UECallback : public aidl::android::os::BnUpdateEngineStableCallback {
   public:
    UECallback() = default;

    // android::os::BnUpdateEngineStableCallback overrides.
    ndk::ScopedAStatus onStatusUpdate(int status_code, float progress) override;
    ndk::ScopedAStatus onPayloadApplicationComplete(int error_code) override;
  };

  static std::vector<std::string> ParseHeaders(const std::string& arg);

  const ndk::ScopedAIBinder_DeathRecipient death_recipient_{
      AIBinder_DeathRecipient_new(&UpdateEngineServiceDied)};
  std::shared_ptr<aidl::android::os::IUpdateEngineStable> service_;
  std::shared_ptr<aidl::android::os::BnUpdateEngineStableCallback> callback_;
};

ndk::ScopedAStatus UpdateEngineClientAndroid::UECallback::onStatusUpdate(
    int status_code, float progress) {
  LOG(INFO) << "onStatusUpdate(" << status_code << ", " << progress << ")";
  return ndk::ScopedAStatus::ok();
}

ndk::ScopedAStatus
UpdateEngineClientAndroid::UECallback::onPayloadApplicationComplete(
    int error_code) {
  LOG(INFO) << "onPayloadApplicationComplete(" << error_code << ")";
  auto code = static_cast<ErrorCode>(error_code);
  Exit((code == ErrorCode::kSuccess || code == ErrorCode::kUpdatedButNotActive)
           ? EX_OK
           : EX_SOFTWARE);
}

int UpdateEngineClientAndroid::Run() {
  service_ = aidl::android::os::IUpdateEngineStable::fromBinder(ndk::SpAIBinder(
      AServiceManager_getService("android.os.UpdateEngineStableService")));
  if (service_ == nullptr) {
    LOG(ERROR)
        << "Failed to get IUpdateEngineStable binder from service manager.";
    return EX_SOFTWARE;
  }

  // Register a callback object with the service.
  callback_ = ndk::SharedRefBase::make<UECallback>();
  bool bound;
  if (!service_->bind(callback_, &bound).isOk() || !bound) {
    LOG(ERROR) << "Failed to bind() the UpdateEngine daemon.";
    return EX_SOFTWARE;
  }

  auto headers = ParseHeaders(FLAGS_headers);
  ndk::ScopedAStatus status;
  const char* payload_path;
  std::string file_prefix = "file://";
  if (android::base::StartsWith(FLAGS_payload, file_prefix)) {
    payload_path = FLAGS_payload.data() + file_prefix.length();
  } else {
    payload_path = FLAGS_payload.data();
  }
  ndk::ScopedFileDescriptor ufd(
      TEMP_FAILURE_RETRY(open(payload_path, O_RDONLY)));
  if (ufd.get() < 0) {
    PLOG(ERROR) << "Can't open " << payload_path;
    return EX_SOFTWARE;
  }
  status = service_->applyPayloadFd(ufd, FLAGS_offset, FLAGS_size, headers);
  if (!status.isOk()) {
    LOG(ERROR) << "Cannot apply payload: " << status.getDescription();
    return EX_SOFTWARE;
  }

  // When following updates status changes, exit if the update_engine daemon
  // dies.
  if (AIBinder_linkToDeath(service_->asBinder().get(),
                           death_recipient_.get(),
                           nullptr) != STATUS_OK) {
    return EX_SOFTWARE;
  }

  return EX_OK;
}

std::vector<std::string> UpdateEngineClientAndroid::ParseHeaders(
    const std::string& arg) {
  std::vector<std::string> lines = android::base::Split(arg, "\n");
  std::vector<std::string> headers;
  for (const auto& line : lines) {
    auto header = android::base::Trim(line);
    if (!header.empty()) {
      headers.push_back(header);
    }
  }
  return headers;
}

}  // namespace chromeos_update_engine::internal

int main(int argc, char** argv) {
  android::base::InitLogging(argv);
  gflags::ParseCommandLineFlags(&argc, &argv, true);

  // Unlike other update_engine* processes that uses message loops,
  // update_engine_stable_client uses a thread pool model. However, number of
  // threads is limited to 1; that is, 0 additional threads should be spawned.
  // This avoids some race conditions.
  if (!ABinderProcess_setThreadPoolMaxThreadCount(0)) {
    LOG(ERROR) << "Cannot set thread pool max thread count";
    return EX_SOFTWARE;
  }
  ABinderProcess_startThreadPool();

  chromeos_update_engine::internal::UpdateEngineClientAndroid client{};
  int code = client.Run();
  if (code != EX_OK)
    return code;

  ABinderProcess_joinThreadPool();
  LOG(ERROR) << "Exited from joinThreadPool.";
  return EX_SOFTWARE;
}
