// Copyright 2015 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 "components/browser_watcher/window_hang_monitor_win.h"

#include "base/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/win/message_window.h"

namespace browser_watcher {

namespace {

HWND FindNamedWindowForProcess(const base::string16 name, base::ProcessId pid) {
  HWND candidate = base::win::MessageWindow::FindWindow(name);
  if (candidate) {
    DWORD actual_process_id = 0;
    ::GetWindowThreadProcessId(candidate, &actual_process_id);
    if (actual_process_id == pid)
      return candidate;
  }
  return nullptr;
}

}  // namespace

WindowHangMonitor::WindowHangMonitor(base::TimeDelta ping_interval,
                                     base::TimeDelta timeout,
                                     const WindowEventCallback& callback)
    : callback_(callback),
      outstanding_ping_(nullptr),
      timer_(false /* don't retain user task */, false /* don't repeat */),
      ping_interval_(ping_interval),
      hang_timeout_(timeout) {
}

WindowHangMonitor::~WindowHangMonitor() {
  if (outstanding_ping_) {
    // We have an outstanding ping, disable it and leak it intentionally as
    // if the callback arrives eventually, it'll cause a use-after-free.
    outstanding_ping_->monitor = nullptr;
    outstanding_ping_ = nullptr;
  }
}

void WindowHangMonitor::Initialize(base::Process process,
                                   const base::string16& window_name) {
  window_name_ = window_name;
  window_process_ = process.Pass();
  timer_.SetTaskRunner(base::MessageLoop::current()->task_runner());

  ScheduleFindWindow();
}

void WindowHangMonitor::ScheduleFindWindow() {
  // TODO(erikwright): We could reduce the polling by using WaitForInputIdle,
  // but it is hard to test (requiring a non-Console executable).
  timer_.Start(
      FROM_HERE, ping_interval_,
      base::Bind(&WindowHangMonitor::PollForWindow, base::Unretained(this)));
}

void WindowHangMonitor::PollForWindow() {
  int exit_code = 0;
  if (window_process_.WaitForExitWithTimeout(base::TimeDelta(), &exit_code)) {
    callback_.Run(WINDOW_NOT_FOUND);
    return;
  }

  HWND hwnd = FindNamedWindowForProcess(window_name_, window_process_.Pid());
  if (hwnd) {
    // Sends a ping and schedules a timeout task. Upon receiving a ping response
    // further pings will be scheduled ad infinitum. Will signal any failure now
    // or later via the callback.
    SendPing(hwnd);
  } else {
    ScheduleFindWindow();
  }
}

void CALLBACK WindowHangMonitor::OnPongReceived(HWND window,
                                                UINT msg,
                                                ULONG_PTR data,
                                                LRESULT lresult) {
  OutstandingPing* outstanding = reinterpret_cast<OutstandingPing*>(data);

  // If the monitor is still around, clear its pointer.
  if (outstanding->monitor)
    outstanding->monitor->outstanding_ping_ = nullptr;

  delete outstanding;
}

void WindowHangMonitor::SendPing(HWND hwnd) {
  // Set up all state ahead of time to allow for the possibility of the callback
  // being invoked from within SendMessageCallback.
  outstanding_ping_ = new OutstandingPing;
  outstanding_ping_->monitor = this;

  // Note that this is racy to |hwnd| having been re-assigned. If that occurs,
  // we might fail to identify the disappearance of the window with this ping.
  // This is acceptable, as the next ping should detect it.
  if (!::SendMessageCallback(hwnd, WM_NULL, 0, 0, &OnPongReceived,
                             reinterpret_cast<ULONG_PTR>(outstanding_ping_))) {
    // Message sending failed, assume the window is no longer valid,
    // issue the callback and stop the polling.
    delete outstanding_ping_;
    outstanding_ping_ = nullptr;

    callback_.Run(WINDOW_VANISHED);
    return;
  }

  // Issue the count-out callback.
  timer_.Start(FROM_HERE, hang_timeout_,
               base::Bind(&WindowHangMonitor::OnHangTimeout,
                          base::Unretained(this), hwnd));
}

void WindowHangMonitor::OnHangTimeout(HWND hwnd) {
  DCHECK(window_process_.IsValid());

  if (outstanding_ping_) {
    // The ping is still outstanding, the window is hung or has vanished.
    // Orphan the outstanding ping. If the callback arrives late, it will
    // delete it, or if the callback never arrives it'll leak.
    outstanding_ping_->monitor = NULL;
    outstanding_ping_ = NULL;

    if (hwnd !=
        FindNamedWindowForProcess(window_name_, window_process_.Pid())) {
      // The window vanished.
      callback_.Run(WINDOW_VANISHED);
    } else {
      // The window hung.
      callback_.Run(WINDOW_HUNG);
    }
  } else {
    // No ping outstanding, window is not yet hung. Schedule the next retry.
    timer_.Start(
        FROM_HERE, hang_timeout_ - ping_interval_,
        base::Bind(&WindowHangMonitor::OnRetryTimeout, base::Unretained(this)));
  }
}

void WindowHangMonitor::OnRetryTimeout() {
  DCHECK(window_process_.IsValid());
  DCHECK(!outstanding_ping_);
  // We can't simply hold onto the previously located HWND due to potential
  // aliasing.
  // 1. The window handle might have been re-assigned to a different window
  //    from the time we found it to the point where we query for its owning
  //    process.
  // 2. The window handle might have been re-assigned to a different process
  //    at any point after we found it.
  HWND hwnd = FindNamedWindowForProcess(window_name_, window_process_.Pid());
  if (hwnd)
    SendPing(hwnd);
  else
    callback_.Run(WINDOW_VANISHED);
}

}  // namespace browser_watcher