blob: 4efd0eeab19cc0ed568322fe3e5954ed6d55845b [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// 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_worker_oop.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/printing/print_backend_service_manager.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/services/printing/public/mojom/print_backend_service.mojom.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "printing/metafile.h"
#include "printing/printed_document.h"
#include "printing/printing_features.h"
#if BUILDFLAG(IS_WIN)
#include "printing/printed_page_win.h"
#endif
using content::BrowserThread;
namespace printing {
namespace {
// Enumeration of printing events when submitting a job to a print driver.
// This must stay in sync with the corresponding histogram in `histograms.xml`.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class PrintOopResult {
kSuccessful = 0,
kCanceled = 1,
kAccessDenied = 2,
kFailed = 3,
kMaxValue = kFailed,
};
constexpr char kPrintOopPrintResultHistogramName[] = "Printing.Oop.PrintResult";
} // namespace
PrintJobWorkerOop::PrintJobWorkerOop(
std::unique_ptr<PrintingContext::Delegate> printing_context_delegate,
std::unique_ptr<PrintingContext> printing_context,
PrintJob* print_job,
mojom::PrintTargetType print_target_type)
: PrintJobWorker(std::move(printing_context_delegate),
std::move(printing_context),
print_job),
print_target_type_(print_target_type) {}
PrintJobWorkerOop::PrintJobWorkerOop(
std::unique_ptr<PrintingContext::Delegate> printing_context_delegate,
std::unique_ptr<PrintingContext> printing_context,
PrintJob* print_job,
bool simulate_spooling_memory_errors)
: PrintJobWorker(std::move(printing_context_delegate),
std::move(printing_context),
print_job),
simulate_spooling_memory_errors_(simulate_spooling_memory_errors) {}
PrintJobWorkerOop::~PrintJobWorkerOop() {
DCHECK(!service_manager_client_id_.has_value());
}
#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
void PrintJobWorkerOop::SetPrintDocumentClient(
PrintBackendServiceManager::ClientId client_id) {
DCHECK(!service_manager_client_id_.has_value());
service_manager_client_id_ = client_id;
}
#endif
void PrintJobWorkerOop::StartPrinting(PrintedDocument* new_document) {
if (!StartPrintingSanityCheck(new_document))
return;
// Do browser-side context setup.
std::u16string document_name = GetDocumentName(new_document);
bool success = SetupDocument(document_name);
DCHECK(success);
// Keep another reference to the document just for OOP. This reference
// ensures the document object is retained even if the job cancels out and
// the reference to it from `PrintJobWorker` is dropped. This guarantees
// that it can still be used in the various asynchronous callbacks.
document_oop_ = new_document;
std::string device_name =
base::UTF16ToUTF8(document_oop_->settings().device_name());
VLOG(1) << "Start printing document " << document_oop_->cookie() << " to "
<< device_name;
// `PrintBackendServiceManager` interactions must happen on the UI thread.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PrintJobWorkerOop::SendStartPrinting,
ui_weak_factory_.GetWeakPtr(), device_name,
document_name));
}
void PrintJobWorkerOop::OnDidStartPrinting(mojom::ResultCode result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (result != mojom::ResultCode::kSuccess) {
PRINTER_LOG(ERROR) << "Error initiating printing via service for document "
<< document_oop_->cookie() << ": " << result;
if (result != mojom::ResultCode::kAccessDenied || !TryRestartPrinting())
NotifyFailure(result);
return;
}
VLOG(1) << "Printing initiated with service for document "
<< document_oop_->cookie();
task_runner()->PostTask(FROM_HERE,
base::BindOnce(&PrintJobWorker::OnNewPage,
worker_weak_factory_.GetWeakPtr()));
}
#if BUILDFLAG(IS_WIN)
void PrintJobWorkerOop::OnDidRenderPrintedPage(uint32_t page_index,
mojom::ResultCode result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (result != mojom::ResultCode::kSuccess) {
// Once an error happens during rendering, there could be multiple calls
// to here as the queue of sent pages all return back with error.
PRINTER_LOG(ERROR)
<< "Error rendering printed page via service for document "
<< document_oop_->cookie() << ": " << result;
NotifyFailure(result);
return;
}
scoped_refptr<PrintedPage> page = document_oop_->GetPage(page_index);
if (!page) {
PRINTER_LOG(ERROR) << "Unable to get page " << page_index
<< " via service for document "
<< document_oop_->cookie();
task_runner()->PostTask(FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::OnFailure,
worker_weak_factory_.GetWeakPtr()));
return;
}
VLOG(1) << "Rendered printed page via service for document "
<< document_oop_->cookie() << " page " << page_index;
// Signal everyone that the page is printed.
print_job()->PostTask(FROM_HERE,
base::BindOnce(&PrintJob::OnPageDone, print_job(),
base::RetainedRef(page)));
++pages_printed_count_;
if (pages_printed_count_ == document_oop_->page_count()) {
// The last page has printed, can proceed to document done processing.
VLOG(1) << "All pages printed for document";
SendDocumentDone();
}
}
#endif // BUILDFLAG(IS_WIN)
void PrintJobWorkerOop::OnDidRenderPrintedDocument(mojom::ResultCode result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (result != mojom::ResultCode::kSuccess) {
PRINTER_LOG(ERROR)
<< "Error rendering printed document via service for document "
<< document_oop_->cookie() << ": " << result;
NotifyFailure(result);
return;
}
VLOG(1) << "Rendered printed document via service for document "
<< document_oop_->cookie();
SendDocumentDone();
}
void PrintJobWorkerOop::OnDidDocumentDone(int job_id,
mojom::ResultCode result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
#if BUILDFLAG(IS_WIN)
DCHECK_EQ(pages_printed_count_, document_oop_->page_count());
#endif
if (result != mojom::ResultCode::kSuccess) {
PRINTER_LOG(ERROR) << "Error completing printing via service for document "
<< document_oop_->cookie() << ": " << result;
NotifyFailure(result);
return;
}
PRINTER_LOG(EVENT) << "Printing completed via service for document "
<< document_oop_->cookie();
UnregisterServiceManagerClient();
base::UmaHistogramEnumeration(kPrintOopPrintResultHistogramName,
PrintOopResult::kSuccessful);
FinishDocumentDone(job_id);
// Also done with private document reference.
document_oop_ = nullptr;
}
void PrintJobWorkerOop::OnDidCancel(scoped_refptr<PrintJob> job) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DVLOG(1) << "Cancel completed for printing via service for document "
<< document_oop_->cookie();
UnregisterServiceManagerClient();
// Done with private document reference.
document_oop_ = nullptr;
}
#if BUILDFLAG(IS_WIN)
bool PrintJobWorkerOop::SpoolPage(PrintedPage* page) {
DCHECK(task_runner()->RunsTasksInCurrentSequence());
DCHECK_NE(page_number(), PageNumber::npos());
#if !defined(NDEBUG)
DCHECK(document_oop_->IsPageInList(*page));
#endif
const MetafilePlayer* metafile = page->metafile();
DCHECK(metafile);
base::MappedReadOnlyRegion region_mapping =
metafile->GetDataAsSharedMemoryRegion();
if (simulate_spooling_memory_errors_ || !region_mapping.IsValid()) {
PRINTER_LOG(ERROR)
<< "Spooling page via service failed due to shared memory error.";
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PrintJobWorkerOop::NotifyFailure,
ui_weak_factory_.GetWeakPtr(),
mojom::ResultCode::kFailed));
return false;
}
VLOG(1) << "Spooling page " << page_number() << " to print via service";
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::SendRenderPrintedPage,
ui_weak_factory_.GetWeakPtr(), base::RetainedRef(page),
metafile->GetDataType(),
std::move(region_mapping.region)));
return true;
}
#endif // BUILDFLAG(IS_WIN)
bool PrintJobWorkerOop::SpoolDocument() {
DCHECK(task_runner()->RunsTasksInCurrentSequence());
const MetafilePlayer* metafile = document_oop_->GetMetafile();
DCHECK(metafile);
base::MappedReadOnlyRegion region_mapping =
metafile->GetDataAsSharedMemoryRegion();
if (simulate_spooling_memory_errors_ || !region_mapping.IsValid()) {
PRINTER_LOG(ERROR)
<< "Spooling document via service failed due to shared memory error.";
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PrintJobWorkerOop::NotifyFailure,
ui_weak_factory_.GetWeakPtr(),
mojom::ResultCode::kFailed));
return false;
}
VLOG(1) << "Spooling job to print via service";
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::SendRenderPrintedDocument,
ui_weak_factory_.GetWeakPtr(), metafile->GetDataType(),
std::move(region_mapping.region)));
return true;
}
void PrintJobWorkerOop::OnDocumentDone() {
// Can do browser-side checks related to completeness for sending, but must
// wait to do OOP related work until OnDidDocumentDone() is received.
CheckDocumentSpoolingComplete();
// Since this call occurs due to all pages having been sent, do not just call
// `SendDocumentDone()`. That should happen as a result of callbacks from
// PrintBackend service.
}
void PrintJobWorkerOop::OnCancel() {
// Retain a reference to the PrintJob to ensure it doesn't get deleted before
// the `OnDidCancel()` callback occurs.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PrintJobWorkerOop::SendCancel,
ui_weak_factory_.GetWeakPtr(),
base::WrapRefCounted(print_job())));
PrintJobWorker::OnCancel();
}
void PrintJobWorkerOop::OnFailure() {
// Retain a reference to the PrintJob to ensure it doesn't get deleted before
// the `OnDidCancel()` callback occurs.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PrintJobWorkerOop::SendCancel,
ui_weak_factory_.GetWeakPtr(),
base::WrapRefCounted(print_job())));
PrintJobWorker::OnFailure();
}
void PrintJobWorkerOop::UnregisterServiceManagerClient() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (service_manager_client_id_.has_value()) {
PrintBackendServiceManager::GetInstance().UnregisterClient(
service_manager_client_id_.value());
service_manager_client_id_.reset();
}
}
bool PrintJobWorkerOop::TryRestartPrinting() {
// Safety precaution to avoid any chance of infinite loop for retrying.
if (print_retried_)
return false;
print_retried_ = true;
// Register that this printer requires elevated privileges.
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
service_mgr.SetPrinterDriverFoundToRequireElevatedPrivilege(device_name_);
// Failure from access-denied means we no longer need the prior client ID.
UnregisterServiceManagerClient();
// Retry the operation, which should now happen at a higher privilege
// level.
SendStartPrinting(device_name_, document_name_);
return true;
}
void PrintJobWorkerOop::NotifyFailure(mojom::ResultCode result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
PrintOopResult uma_result = PrintOopResult::kFailed;
if (result == mojom::ResultCode::kAccessDenied) {
// An attempt to restart could be undesirable if some pages were able to
// be sent to the destination before the error occurred. If we receive
// an access-denied error in such cases then we just abort this print job
// with an error notification to the user. This is more clear to the user
// what has occurred than if we transparently retry the job and succeed,
// where the user could end up with too many printed pages and not know
// why.
// Register that this printer requires elevated privileges so that any
// further attempts to print should succeed.
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
service_mgr.SetPrinterDriverFoundToRequireElevatedPrivilege(device_name_);
uma_result = PrintOopResult::kAccessDenied;
} else if (result == mojom::ResultCode::kCanceled) {
uma_result = PrintOopResult::kCanceled;
}
base::UmaHistogramEnumeration(kPrintOopPrintResultHistogramName, uma_result);
// Initiate rest of regular failure handling.
if (result == mojom::ResultCode::kCanceled) {
task_runner()->PostTask(FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::OnCancel,
worker_weak_factory_.GetWeakPtr()));
} else {
task_runner()->PostTask(FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::OnFailure,
worker_weak_factory_.GetWeakPtr()));
}
}
void PrintJobWorkerOop::SendStartPrinting(const std::string& device_name,
const std::u16string& document_name) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(features::kEnableOopPrintDriversJobPrint.Get());
// The device name is needed repeatedly for each call to the service, cache
// that for this print job.
device_name_ = device_name;
// Save the document name in case it is needed for retrying a job after
// failure.
document_name_ = document_name;
const int32_t document_cookie = document_oop_->cookie();
PRINTER_LOG(DEBUG) << "Starting printing via service for to `" << device_name_
<< "` for document " << document_cookie;
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
// Register this worker as a printing client, if registration wasn't already
// performed earlier.
if (!service_manager_client_id_.has_value()) {
service_manager_client_id_ =
service_mgr.RegisterPrintDocumentClient(device_name_);
}
service_mgr.StartPrinting(
device_name_, document_cookie, document_name_, print_target_type_,
document_oop_->settings(),
base::BindOnce(&PrintJobWorkerOop::OnDidStartPrinting,
ui_weak_factory_.GetWeakPtr()));
}
#if BUILDFLAG(IS_WIN)
void PrintJobWorkerOop::SendRenderPrintedPage(
const PrintedPage* page,
mojom::MetafileDataType page_data_type,
base::ReadOnlySharedMemoryRegion serialized_page_data) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Page numbers are 0-based for the printing context.
const uint32_t page_index = page->page_number() - 1;
const int32_t document_cookie = document_oop_->cookie();
VLOG(1) << "Sending page " << page_index << " of document " << document_cookie
<< " to `" << device_name_ << "` for printing";
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
service_mgr.RenderPrintedPage(
device_name_, document_cookie, *page, page_data_type,
std::move(serialized_page_data),
base::BindOnce(&PrintJobWorkerOop::OnDidRenderPrintedPage,
ui_weak_factory_.GetWeakPtr(), page_index));
}
#endif // BUILDFLAG(IS_WIN)
void PrintJobWorkerOop::SendRenderPrintedDocument(
mojom::MetafileDataType data_type,
base::ReadOnlySharedMemoryRegion serialized_data) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const int32_t document_cookie = document_oop_->cookie();
VLOG(1) << "Sending document " << document_cookie << " to `" << device_name_
<< "` for printing";
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
service_mgr.RenderPrintedDocument(
device_name_, document_cookie, document_oop_->page_count(), data_type,
std::move(serialized_data),
base::BindOnce(&PrintJobWorkerOop::OnDidRenderPrintedDocument,
ui_weak_factory_.GetWeakPtr()));
}
void PrintJobWorkerOop::SendDocumentDone() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const int32_t document_cookie = document_oop_->cookie();
VLOG(1) << "Sending document done for document " << document_cookie;
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
service_mgr.DocumentDone(device_name_, document_cookie,
base::BindOnce(&PrintJobWorkerOop::OnDidDocumentDone,
ui_weak_factory_.GetWeakPtr(),
printing_context()->job_id()));
}
void PrintJobWorkerOop::SendCancel(scoped_refptr<PrintJob> job) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// If an error has occurred during rendering in middle of a multi-page job,
// it could be possible for the `OnDidRenderPrintedPage()` callback of latter
// pages to still go through error processing. In such a case the document
// might already have been canceled, so we should ensure to only send a
// cancel request to the service if we haven't already done so.
if (print_cancel_requested_)
return;
print_cancel_requested_ = true;
VLOG(1) << "Sending cancel for document " << document_oop_->cookie();
PrintBackendServiceManager& service_mgr =
PrintBackendServiceManager::GetInstance();
// Retain a reference to the PrintJob to ensure it doesn't get deleted before
// the `OnDidCancel()` callback occurs.
service_mgr.Cancel(
device_name_, document_oop_->cookie(),
base::BindOnce(&PrintJobWorkerOop::OnDidCancel,
ui_weak_factory_.GetWeakPtr(), std::move(job)));
}
} // namespace printing