// 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(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(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 details( new JobEventDetails(JobEventDetails::NEW_DOC, document_.get(), NULL)); NotificationService::current()->Notify( NotificationType::PRINT_JOB_EVENT, Source(this), Details(details.get())); } void PrintJob::Stop() { DCHECK_EQ(ui_message_loop_, MessageLoop::current()); // Be sure to live long enough. scoped_refptr handle(this); MessageLoop* worker_loop = worker_->message_loop(); if (worker_loop) { ControlledWorkerShutdown(); is_job_pending_ = false; registrar_.Remove(this, NotificationType::PRINT_JOB_EVENT, Source(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 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 details( new JobEventDetails(JobEventDetails::FAILED, NULL, NULL)); NotificationService::current()->Notify( NotificationType::PRINT_JOB_EVENT, Source(this), Details(details.get())); Stop(); is_canceling_ = false; } bool PrintJob::FlushJob(int timeout_ms) { // Make sure the object outlive this message loop. scoped_refptr 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 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 handle(this); // Stop the worker thread. Stop(); scoped_refptr details( new JobEventDetails(JobEventDetails::JOB_DONE, document_.get(), NULL)); NotificationService::current()->Notify( NotificationType::PRINT_JOB_EVENT, Source(this), Details(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