// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/files/file_path_watcher.h"

#include "base/file_path.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/message_loop_proxy.h"
#include "base/time.h"
#include "base/win/object_watcher.h"

namespace base {
namespace files {

namespace {

class FilePathWatcherImpl : public FilePathWatcher::PlatformDelegate,
                            public base::win::ObjectWatcher::Delegate,
                            public MessageLoop::DestructionObserver {
 public:
  FilePathWatcherImpl() : delegate_(NULL), handle_(INVALID_HANDLE_VALUE) {}

  // FilePathWatcher::PlatformDelegate overrides.
  virtual bool Watch(const FilePath& path,
                     FilePathWatcher::Delegate* delegate) OVERRIDE;
  virtual void Cancel() OVERRIDE;

  // Deletion of the FilePathWatcher will call Cancel() to dispose of this
  // object in the right thread. This also observes destruction of the required
  // cleanup thread, in case it quits before Cancel() is called.
  virtual void WillDestroyCurrentMessageLoop() OVERRIDE;

  // Callback from MessageLoopForIO.
  virtual void OnObjectSignaled(HANDLE object);

 private:
  virtual ~FilePathWatcherImpl() {}

  // Setup a watch handle for directory |dir|. Returns true if no fatal error
  // occurs. |handle| will receive the handle value if |dir| is watchable,
  // otherwise INVALID_HANDLE_VALUE.
  static bool SetupWatchHandle(const FilePath& dir, HANDLE* handle)
      WARN_UNUSED_RESULT;

  // (Re-)Initialize the watch handle.
  bool UpdateWatch() WARN_UNUSED_RESULT;

  // Destroy the watch handle.
  void DestroyWatch();

  // Cleans up and stops observing the |message_loop_| thread.
  void CancelOnMessageLoopThread() OVERRIDE;

  // Delegate to notify upon changes.
  scoped_refptr<FilePathWatcher::Delegate> delegate_;

  // Path we're supposed to watch (passed to delegate).
  FilePath target_;

  // Handle for FindFirstChangeNotification.
  HANDLE handle_;

  // ObjectWatcher to watch handle_ for events.
  base::win::ObjectWatcher watcher_;

  // Keep track of the last modified time of the file.  We use nulltime
  // to represent the file not existing.
  base::Time last_modified_;

  // The time at which we processed the first notification with the
  // |last_modified_| time stamp.
  base::Time first_notification_;

  DISALLOW_COPY_AND_ASSIGN(FilePathWatcherImpl);
};

bool FilePathWatcherImpl::Watch(const FilePath& path,
                                FilePathWatcher::Delegate* delegate) {
  DCHECK(target_.value().empty());  // Can only watch one path.

  set_message_loop(base::MessageLoopProxy::CreateForCurrentThread());
  delegate_ = delegate;
  target_ = path;
  MessageLoop::current()->AddDestructionObserver(this);

  if (!UpdateWatch())
    return false;

  watcher_.StartWatching(handle_, this);

  return true;
}

void FilePathWatcherImpl::Cancel() {
  if (!delegate_) {
    // Watch was never called, or the |message_loop_| has already quit.
    set_cancelled();
    return;
  }

  // Switch to the file thread if necessary so we can stop |watcher_|.
  if (!message_loop()->BelongsToCurrentThread()) {
    message_loop()->PostTask(FROM_HERE,
                             new FilePathWatcher::CancelTask(this));
  } else {
    CancelOnMessageLoopThread();
  }
}

void FilePathWatcherImpl::CancelOnMessageLoopThread() {
  set_cancelled();

  if (handle_ != INVALID_HANDLE_VALUE)
    DestroyWatch();

  if (delegate_) {
    MessageLoop::current()->RemoveDestructionObserver(this);
    delegate_ = NULL;
  }
}

void FilePathWatcherImpl::WillDestroyCurrentMessageLoop() {
  CancelOnMessageLoopThread();
}

void FilePathWatcherImpl::OnObjectSignaled(HANDLE object) {
  DCHECK(object == handle_);
  // Make sure we stay alive through the body of this function.
  scoped_refptr<FilePathWatcherImpl> keep_alive(this);

  if (!UpdateWatch()) {
    delegate_->OnFilePathError(target_);
    return;
  }

  // Check whether the event applies to |target_| and notify the delegate.
  base::PlatformFileInfo file_info;
  bool file_exists = file_util::GetFileInfo(target_, &file_info);
  if (file_exists && (last_modified_.is_null() ||
      last_modified_ != file_info.last_modified)) {
    last_modified_ = file_info.last_modified;
    first_notification_ = base::Time::Now();
    delegate_->OnFilePathChanged(target_);
  } else if (file_exists && !first_notification_.is_null()) {
    // The target's last modification time is equal to what's on record. This
    // means that either an unrelated event occurred, or the target changed
    // again (file modification times only have a resolution of 1s). Comparing
    // file modification times against the wall clock is not reliable to find
    // out whether the change is recent, since this code might just run too
    // late. Moreover, there's no guarantee that file modification time and wall
    // clock times come from the same source.
    //
    // Instead, the time at which the first notification carrying the current
    // |last_notified_| time stamp is recorded. Later notifications that find
    // the same file modification time only need to be forwarded until wall
    // clock has advanced one second from the initial notification. After that
    // interval, client code is guaranteed to having seen the current revision
    // of the file.
    if (base::Time::Now() - first_notification_ >
        base::TimeDelta::FromSeconds(1)) {
      // Stop further notifications for this |last_modification_| time stamp.
      first_notification_ = base::Time();
    }
    delegate_->OnFilePathChanged(target_);
  } else if (!file_exists && !last_modified_.is_null()) {
    last_modified_ = base::Time();
    delegate_->OnFilePathChanged(target_);
  }

  // The watch may have been cancelled by the callback.
  if (handle_ != INVALID_HANDLE_VALUE)
    watcher_.StartWatching(handle_, this);
}

// static
bool FilePathWatcherImpl::SetupWatchHandle(const FilePath& dir,
                                           HANDLE* handle) {
  *handle = FindFirstChangeNotification(
      dir.value().c_str(),
      false,  // Don't watch subtrees
      FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE |
      FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_DIR_NAME |
      FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY);
  if (*handle != INVALID_HANDLE_VALUE) {
    // Make sure the handle we got points to an existing directory. It seems
    // that windows sometimes hands out watches to direectories that are
    // about to go away, but doesn't sent notifications if that happens.
    if (!file_util::DirectoryExists(dir)) {
      FindCloseChangeNotification(*handle);
      *handle = INVALID_HANDLE_VALUE;
    }
    return true;
  }

  // If FindFirstChangeNotification failed because the target directory
  // doesn't exist, access is denied (happens if the file is already gone but
  // there are still handles open), or the target is not a directory, try the
  // immediate parent directory instead.
  DWORD error_code = GetLastError();
  if (error_code != ERROR_FILE_NOT_FOUND &&
      error_code != ERROR_PATH_NOT_FOUND &&
      error_code != ERROR_ACCESS_DENIED &&
      error_code != ERROR_SHARING_VIOLATION &&
      error_code != ERROR_DIRECTORY) {
    using ::operator<<; // Pick the right operator<< below.
    PLOG(ERROR) << "FindFirstChangeNotification failed for "
                << dir.value();
    return false;
  }

  return true;
}

bool FilePathWatcherImpl::UpdateWatch() {
  if (handle_ != INVALID_HANDLE_VALUE)
    DestroyWatch();

  base::PlatformFileInfo file_info;
  if (file_util::GetFileInfo(target_, &file_info)) {
    last_modified_ = file_info.last_modified;
    first_notification_ = base::Time::Now();
  }

  // Start at the target and walk up the directory chain until we succesfully
  // create a watch handle in |handle_|. |child_dirs| keeps a stack of child
  // directories stripped from target, in reverse order.
  std::vector<FilePath> child_dirs;
  FilePath watched_path(target_);
  while (true) {
    if (!SetupWatchHandle(watched_path, &handle_))
      return false;

    // Break if a valid handle is returned. Try the parent directory otherwise.
    if (handle_ != INVALID_HANDLE_VALUE)
      break;

    // Abort if we hit the root directory.
    child_dirs.push_back(watched_path.BaseName());
    FilePath parent(watched_path.DirName());
    if (parent == watched_path) {
      LOG(ERROR) << "Reached the root directory";
      return false;
    }
    watched_path = parent;
  }

  // At this point, handle_ is valid. However, the bottom-up search that the
  // above code performs races against directory creation. So try to walk back
  // down and see whether any children appeared in the mean time.
  while (!child_dirs.empty()) {
    watched_path = watched_path.Append(child_dirs.back());
    child_dirs.pop_back();
    HANDLE temp_handle = INVALID_HANDLE_VALUE;
    if (!SetupWatchHandle(watched_path, &temp_handle))
      return false;
    if (temp_handle == INVALID_HANDLE_VALUE)
      break;
    FindCloseChangeNotification(handle_);
    handle_ = temp_handle;
  }

  return true;
}

void FilePathWatcherImpl::DestroyWatch() {
  watcher_.StopWatching();
  FindCloseChangeNotification(handle_);
  handle_ = INVALID_HANDLE_VALUE;
}

}  // namespace

FilePathWatcher::FilePathWatcher() {
  impl_ = new FilePathWatcherImpl();
}

}  // namespace files
}  // namespace base
