// 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 "chrome/browser/printing/print_job.h"
#include "base/message_loop.h"
#include "base/threading/thread_restrictions.h"
#include "base/timer.h"
#include "chrome/browser/printing/print_job_worker.h"
#include "content/common/notification_service.h"
#include "printing/printed_document.h"
#include "printing/printed_page.h"
using base::TimeDelta;
namespace printing {
PrintJob::PrintJob()
: ui_message_loop_(MessageLoop::current()),
source_(NULL),
worker_(),
settings_(),
is_job_pending_(false),
is_canceling_(false) {
DCHECK(ui_message_loop_);
// This is normally a UI message loop, but in unit tests, the message loop is
// of the 'default' type.
DCHECK(ui_message_loop_->type() == MessageLoop::TYPE_UI ||
ui_message_loop_->type() == MessageLoop::TYPE_DEFAULT);
ui_message_loop_->AddDestructionObserver(this);
}
PrintJob::~PrintJob() {
ui_message_loop_->RemoveDestructionObserver(this);
// The job should be finished (or at least canceled) when it is destroyed.
DCHECK(!is_job_pending_);
DCHECK(!is_canceling_);
if (worker_.get())
DCHECK(worker_->message_loop() == NULL);
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
}
void PrintJob::Initialize(PrintJobWorkerOwner* job,
PrintedPagesSource* source,
int page_count) {
DCHECK(!source_);
DCHECK(!worker_.get());
DCHECK(!is_job_pending_);
DCHECK(!is_canceling_);
DCHECK(!document_.get());
source_ = source;
worker_.reset(job->DetachWorker(this));
settings_ = job->settings();
PrintedDocument* new_doc =
new PrintedDocument(settings_, source_, job->cookie());
new_doc->set_page_count(page_count);
UpdatePrintedDocument(new_doc);
// Don't forget to register to our own messages.
registrar_.Add(this, NotificationType::PRINT_JOB_EVENT,
Source<PrintJob>(this));
}
void PrintJob::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
switch (type.value) {
case NotificationType::PRINT_JOB_EVENT: {
OnNotifyPrintJobEvent(*Details<JobEventDetails>(details).ptr());
break;
}
default: {
break;
}
}
}
void PrintJob::GetSettingsDone(const PrintSettings& new_settings,
PrintingContext::Result result) {
NOTREACHED();
}
PrintJobWorker* PrintJob::DetachWorker(PrintJobWorkerOwner* new_owner) {
NOTREACHED();
return NULL;
}
MessageLoop* PrintJob::message_loop() {
return ui_message_loop_;
}
const PrintSettings& PrintJob::settings() const {
return settings_;
}
int PrintJob::cookie() const {
if (!document_.get())
// Always use an invalid cookie in this case.
return 0;
return document_->cookie();
}
void PrintJob::WillDestroyCurrentMessageLoop() {
NOTREACHED();
}
void PrintJob::StartPrinting() {
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
DCHECK(worker_->message_loop());
DCHECK(!is_job_pending_);
if (!worker_->message_loop() || is_job_pending_)
return;
// Real work is done in PrintJobWorker::StartPrinting().
worker_->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
worker_.get(), &PrintJobWorker::StartPrinting, document_));
// Set the flag right now.
is_job_pending_ = true;
// Tell everyone!
scoped_refptr<JobEventDetails> details(
new JobEventDetails(JobEventDetails::NEW_DOC, document_.get(), NULL));
NotificationService::current()->Notify(
NotificationType::PRINT_JOB_EVENT,
Source<PrintJob>(this),
Details<JobEventDetails>(details.get()));
}
void PrintJob::Stop() {
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
// Be sure to live long enough.
scoped_refptr<PrintJob> handle(this);
MessageLoop* worker_loop = worker_->message_loop();
if (worker_loop) {
ControlledWorkerShutdown();
is_job_pending_ = false;
registrar_.Remove(this, NotificationType::PRINT_JOB_EVENT,
Source<PrintJob>(this));
}
// Flush the cached document.
UpdatePrintedDocument(NULL);
}
void PrintJob::Cancel() {
if (is_canceling_)
return;
is_canceling_ = true;
// Be sure to live long enough.
scoped_refptr<PrintJob> handle(this);
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
MessageLoop* worker_loop = worker_.get() ? worker_->message_loop() : NULL;
if (worker_loop) {
// Call this right now so it renders the context invalid. Do not use
// InvokeLater since it would take too much time.
worker_->Cancel();
}
// Make sure a Cancel() is broadcast.
scoped_refptr<JobEventDetails> details(
new JobEventDetails(JobEventDetails::FAILED, NULL, NULL));
NotificationService::current()->Notify(
NotificationType::PRINT_JOB_EVENT,
Source<PrintJob>(this),
Details<JobEventDetails>(details.get()));
Stop();
is_canceling_ = false;
}
bool PrintJob::FlushJob(int timeout_ms) {
// Make sure the object outlive this message loop.
scoped_refptr<PrintJob> handle(this);
// Stop() will eventually be called, which will get out of the inner message
// loop. But, don't take it for granted and set a timer in case something goes
// wrong.
base::OneShotTimer<MessageLoop> quit_task;
if (timeout_ms) {
quit_task.Start(TimeDelta::FromMilliseconds(timeout_ms),
MessageLoop::current(), &MessageLoop::Quit);
}
bool old_state = MessageLoop::current()->NestableTasksAllowed();
MessageLoop::current()->SetNestableTasksAllowed(true);
MessageLoop::current()->Run();
// Restore task state.
MessageLoop::current()->SetNestableTasksAllowed(old_state);
return true;
}
void PrintJob::DisconnectSource() {
source_ = NULL;
if (document_.get())
document_->DisconnectSource();
}
bool PrintJob::is_job_pending() const {
return is_job_pending_;
}
PrintedDocument* PrintJob::document() const {
return document_.get();
}
void PrintJob::UpdatePrintedDocument(PrintedDocument* new_document) {
if (document_.get() == new_document)
return;
document_ = new_document;
if (document_.get()) {
settings_ = document_->settings();
}
if (worker_.get() && worker_->message_loop()) {
DCHECK(!is_job_pending_);
// Sync the document with the worker.
worker_->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
worker_.get(), &PrintJobWorker::OnDocumentChanged, document_));
}
}
void PrintJob::OnNotifyPrintJobEvent(const JobEventDetails& event_details) {
switch (event_details.type()) {
case JobEventDetails::FAILED: {
settings_.Clear();
// No need to cancel since the worker already canceled itself.
Stop();
break;
}
case JobEventDetails::USER_INIT_DONE:
case JobEventDetails::DEFAULT_INIT_DONE:
case JobEventDetails::USER_INIT_CANCELED: {
DCHECK_EQ(event_details.document(), document_.get());
break;
}
case JobEventDetails::NEW_DOC:
case JobEventDetails::NEW_PAGE:
case JobEventDetails::PAGE_DONE:
case JobEventDetails::JOB_DONE:
case JobEventDetails::ALL_PAGES_REQUESTED: {
// Don't care.
break;
}
case JobEventDetails::DOC_DONE: {
// This will call Stop() and broadcast a JOB_DONE message.
MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(
this, &PrintJob::OnDocumentDone));
break;
}
default: {
NOTREACHED();
break;
}
}
}
void PrintJob::OnDocumentDone() {
// Be sure to live long enough. The instance could be destroyed by the
// JOB_DONE broadcast.
scoped_refptr<PrintJob> handle(this);
// Stop the worker thread.
Stop();
scoped_refptr<JobEventDetails> details(
new JobEventDetails(JobEventDetails::JOB_DONE, document_.get(), NULL));
NotificationService::current()->Notify(
NotificationType::PRINT_JOB_EVENT,
Source<PrintJob>(this),
Details<JobEventDetails>(details.get()));
}
void PrintJob::ControlledWorkerShutdown() {
DCHECK_EQ(ui_message_loop_, MessageLoop::current());
// The deadlock this code works around is specific to window messaging on
// Windows, so we aren't likely to need it on any other platforms.
#if defined(OS_WIN)
// We could easily get into a deadlock case if worker_->Stop() is used; the
// printer driver created a window as a child of the browser window. By
// canceling the job, the printer driver initiated dialog box is destroyed,
// which sends a blocking message to its parent window. If the browser window
// thread is not processing messages, a deadlock occurs.
//
// This function ensures that the dialog box will be destroyed in a timely
// manner by the mere fact that the thread will terminate. So the potential
// deadlock is eliminated.
worker_->StopSoon();
// Run a tight message loop until the worker terminates. It may seems like a
// hack but I see no other way to get it to work flawlessly. The issues here
// are:
// - We don't want to run tasks while the thread is quitting.
// - We want this code path to wait on the thread to quit before continuing.
MSG msg;
HANDLE thread_handle = worker_->thread_handle();
for (; thread_handle;) {
// Note that we don't do any kind of message priorization since we don't
// execute any pending task or timer.
DWORD result = MsgWaitForMultipleObjects(1, &thread_handle,
FALSE, INFINITE, QS_ALLINPUT);
if (result == WAIT_OBJECT_0 + 1) {
while (PeekMessage(&msg, NULL, 0, 0, TRUE) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Continue looping.
} else if (result == WAIT_OBJECT_0) {
// The thread quit.
break;
} else {
// An error occured. Assume the thread quit.
NOTREACHED();
break;
}
}
#endif
// Temporarily allow it until we fix
// http://code.google.com/p/chromium/issues/detail?id=67044
base::ThreadRestrictions::ScopedAllowIO allow_io;
// Now make sure the thread object is cleaned up.
worker_->Stop();
}
// Takes settings_ ownership and will be deleted in the receiving thread.
JobEventDetails::JobEventDetails(Type type,
PrintedDocument* document,
PrintedPage* page)
: document_(document),
page_(page),
type_(type) {
}
JobEventDetails::~JobEventDetails() {
}
PrintedDocument* JobEventDetails::document() const {
return document_;
}
PrintedPage* JobEventDetails::page() const {
return page_;
}
} // namespace printing