// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/files/important_file_writer_cleaner.h"

#include <algorithm>
#include <functional>
#include <iterator>
#include <utility>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/process/process.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/build_config.h"

namespace base {

namespace {

base::Time GetUpperBoundTime() {
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS) || BUILDFLAG(IS_FUCHSIA)
  // If process creation time is not available then use instance creation
  // time as the upper-bound for old files. Modification times may be
  // rounded-down to coarse-grained increments, e.g. FAT has 2s granularity,
  // so it is necessary to set the upper-bound earlier than Now() by at least
  // that margin to account for modification times being rounded-down.
  return Time::Now() - Seconds(2);
#else
  return Process::Current().CreationTime() - Seconds(2);
#endif
}

}  // namespace

// static
ImportantFileWriterCleaner& ImportantFileWriterCleaner::GetInstance() {
  static NoDestructor<ImportantFileWriterCleaner> instance;
  return *instance;
}

// static
void ImportantFileWriterCleaner::AddDirectory(const FilePath& directory) {
  auto& instance = GetInstance();
  scoped_refptr<SequencedTaskRunner> task_runner;
  {
    AutoLock scoped_lock(instance.task_runner_lock_);
    task_runner = instance.task_runner_;
  }
  if (!task_runner)
    return;
  if (task_runner->RunsTasksInCurrentSequence()) {
    instance.AddDirectoryImpl(directory);
  } else {
    // Unretained is safe here since the cleaner instance is never destroyed.
    task_runner->PostTask(
        FROM_HERE, BindOnce(&ImportantFileWriterCleaner::AddDirectoryImpl,
                            Unretained(&instance), directory));
  }
}

void ImportantFileWriterCleaner::Initialize() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  AutoLock scoped_lock(task_runner_lock_);
  DCHECK(!task_runner_ ||
         task_runner_ == SequencedTaskRunner::GetCurrentDefault());
  task_runner_ = SequencedTaskRunner::GetCurrentDefault();
}

void ImportantFileWriterCleaner::Start() {
#if DCHECK_IS_ON()
  {
    AutoLock scoped_lock(task_runner_lock_);
    DCHECK(task_runner_);
  }
#endif
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (is_started())
    return;

  started_ = true;

  if (!pending_directories_.empty())
    ScheduleTask();
}

void ImportantFileWriterCleaner::Stop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!is_started())
    return;

  if (is_running())
    stop_flag_.store(true, std::memory_order_relaxed);
  else
    DoStop();
}

void ImportantFileWriterCleaner::UninitializeForTesting() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!is_started());
  {
    AutoLock scoped_lock(task_runner_lock_);
    task_runner_ = nullptr;
  }
  // AddDirectory may have been called after Stop. Clear the containers just in
  // case.
  important_directories_.clear();
  pending_directories_.clear();
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

base::Time ImportantFileWriterCleaner::GetUpperBoundTimeForTest() const {
  return upper_bound_time_;
}

ImportantFileWriterCleaner::ImportantFileWriterCleaner()
    : upper_bound_time_(GetUpperBoundTime()) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

void ImportantFileWriterCleaner::AddDirectoryImpl(const FilePath& directory) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!important_directories_.insert(directory).second)
    return;  // This directory has already been seen.

  pending_directories_.push_back(directory);

  if (!is_started())
    return;  // Nothing more to do if Start() has not been called.

  // Start the background task if it's not already running. If it is running, a
  // new task will be posted on completion of the current one by
  // OnBackgroundTaskFinished to handle all directories added while it was
  // running.
  if (!is_running())
    ScheduleTask();
}

void ImportantFileWriterCleaner::ScheduleTask() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(is_started());
  DCHECK(!is_running());
  DCHECK(!pending_directories_.empty());
  DCHECK(!stop_flag_.load(std::memory_order_relaxed));

  // Pass the set of directories to be processed.
  running_ = ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {TaskPriority::BEST_EFFORT, TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
       MayBlock()},
      BindOnce(&ImportantFileWriterCleaner::CleanInBackground,
               upper_bound_time_, std::move(pending_directories_),
               std::ref(stop_flag_)),
      // Unretained is safe here since the cleaner instance is never destroyed.
      BindOnce(&ImportantFileWriterCleaner::OnBackgroundTaskFinished,
               Unretained(this)));
}

// static
bool ImportantFileWriterCleaner::CleanInBackground(
    Time upper_bound_time,
    std::vector<FilePath> directories,
    std::atomic_bool& stop_flag) {
  DCHECK(!directories.empty());
  for (auto scan = directories.begin(), end = directories.end(); scan != end;
       ++scan) {
    const auto& directory = *scan;
    FileEnumerator file_enum(
        directory, /*recursive=*/false, FileEnumerator::FILES,
        FormatTemporaryFileName(FILE_PATH_LITERAL("*")).value());
    for (FilePath path = file_enum.Next(); !path.empty();
         path = file_enum.Next()) {
      const FileEnumerator::FileInfo info = file_enum.GetInfo();
      if (info.GetLastModifiedTime() >= upper_bound_time)
        continue;
      // Cleanup is a best-effort process, so ignore any failures here and
      // continue to clean as much as possible. Metrics tell us that ~98.4% of
      // directories are cleaned with no failures.
      DeleteFile(path);
      // Break out without checking for the next file if a stop is requested.
      if (stop_flag.load(std::memory_order_relaxed))
        return false;
    }
  }
  return true;
}

void ImportantFileWriterCleaner::OnBackgroundTaskFinished(
    bool processing_completed) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  running_ = false;

  // There are no other accessors of |stop_flag_| at this point, so atomic
  // operations aren't needed. There is no way to read it without such, so use
  // the same (relaxed) ordering as elsewhere.
  const bool stop = stop_flag_.exchange(false, std::memory_order_relaxed);
  DCHECK(stop || processing_completed);

  if (stop) {
    DoStop();
  } else if (!pending_directories_.empty()) {
    // Run the task again with the new directories.
    ScheduleTask();
  }  // else do nothing until a new directory is added.
}

void ImportantFileWriterCleaner::DoStop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(is_started());
  DCHECK(!is_running());

  important_directories_.clear();
  pending_directories_.clear();
  started_ = false;
}

}  // namespace base
