| // 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 <optional> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/functional/bind.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/oop_features.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 "components/enterprise/buildflags/buildflags.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/buildflags/buildflags.h" |
| #include "printing/metafile.h" |
| #include "printing/printed_document.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, |
| std::optional<PrintBackendServiceManager::ClientId> client_id, |
| std::optional<PrintBackendServiceManager::ContextId> context_id, |
| PrintJob* print_job, |
| bool print_from_system_dialog) |
| : PrintJobWorkerOop(std::move(printing_context_delegate), |
| std::move(printing_context), |
| client_id, |
| context_id, |
| print_job, |
| print_from_system_dialog, |
| /*simulate_spooling_memory_errors=*/false) {} |
| |
| PrintJobWorkerOop::PrintJobWorkerOop( |
| std::unique_ptr<PrintingContext::Delegate> printing_context_delegate, |
| std::unique_ptr<PrintingContext> printing_context, |
| std::optional<PrintBackendServiceManager::ClientId> client_id, |
| std::optional<PrintBackendServiceManager::ContextId> context_id, |
| PrintJob* print_job, |
| bool print_from_system_dialog, |
| 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), |
| service_manager_client_id_(client_id), |
| printing_context_id_(context_id), |
| print_from_system_dialog_(print_from_system_dialog) {} |
| |
| PrintJobWorkerOop::~PrintJobWorkerOop() { |
| DCHECK(!service_manager_client_id_.has_value()); |
| } |
| |
| 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::Cancel() { |
| PrintJobWorker::Cancel(); |
| PrintJobWorkerOop::OnCancel(); |
| } |
| |
| #if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) |
| void PrintJobWorkerOop::CleanupAfterContentAnalysisDenial() { |
| PrintJobWorker::CleanupAfterContentAnalysisDenial(); |
| UnregisterServiceManagerClient(); |
| } |
| #endif |
| |
| void PrintJobWorkerOop::OnDidStartPrinting(mojom::ResultCode result, |
| int job_id) { |
| 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; |
| } |
| |
| // Retain the job ID that was set in the service. |
| printing_context()->SetJobId(job_id); |
| |
| 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(); |
| NotifyFailure(mojom::ResultCode::kFailed); |
| 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_->expected_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_->expected_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); |
| task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&PrintJobWorkerOop::FinishDocumentDone, |
| worker_weak_factory_.GetWeakPtr(), job_id)); |
| |
| // Also done with private document reference. |
| document_oop_ = nullptr; |
| } |
| |
| void PrintJobWorkerOop::OnDidCancel(scoped_refptr<PrintJob> job, |
| mojom::ResultCode cancel_reason) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DVLOG(1) << "Cancel completed for printing via service for document " |
| << document_oop_->cookie(); |
| |
| UnregisterServiceManagerClient(); |
| printing_context_id_.reset(); |
| |
| if (cancel_reason == mojom::ResultCode::kCanceled) { |
| print_job()->PostTask(FROM_HERE, |
| base::BindOnce(&PrintJob::Cancel, std::move(job))); |
| } else { |
| PostTask(FROM_HERE, base::BindOnce( |
| [](base::WeakPtr<PrintJobWorkerOop> self, |
| scoped_refptr<PrintJob> job) { |
| self->PrintJobWorker::OnFailure(); |
| }, |
| worker_weak_factory_.GetWeakPtr(), std::move(job))); |
| } |
| // 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()->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()->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::FinishDocumentDone(int job_id) { |
| // Helper function to get onto worker thread, since using the protected base |
| // class method directly with `base::BindOnce()` calls is not allowed. |
| PrintJobWorker::FinishDocumentDone(job_id); |
| } |
| |
| void PrintJobWorkerOop::OnCancel() { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&PrintJobWorkerOop::NotifyFailure, |
| ui_weak_factory_.GetWeakPtr(), |
| mojom::ResultCode::kCanceled)); |
| } |
| |
| void PrintJobWorkerOop::OnFailure() { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&PrintJobWorkerOop::NotifyFailure, |
| ui_weak_factory_.GetWeakPtr(), |
| mojom::ResultCode::kFailed)); |
| } |
| |
| 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_); |
| |
| #if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG) |
| if (print_from_system_dialog_) { |
| // Cannot print from process where system dialog was displayed. Another |
| // print attempt where dialog is used again will be required. |
| PRINTER_LOG(ERROR) << "Failure during system printing, unable to retry."; |
| return false; |
| } |
| #endif |
| |
| // Failure from access-denied means we no longer need the prior client ID. |
| UnregisterServiceManagerClient(); |
| |
| // Need to establish a new persistent printing context to use during |
| // printing. Then retry the operation, which should now happen at a higher |
| // privilege level. |
| SendEstablishPrintingContext(); |
| SendStartPrinting(device_name_, document_name_); |
| return true; |
| } |
| |
| void PrintJobWorkerOop::NotifyFailure(mojom::ResultCode result) { |
| DCHECK_CURRENTLY_ON(content::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; |
| |
| 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); |
| |
| if (!document_oop_) { |
| // If no document has been started for printing then don't send cancel. |
| return; |
| } |
| |
| // Initiate rest of regular failure handling. |
| SendCancel(base::BindOnce(&PrintJobWorkerOop::OnDidCancel, |
| ui_weak_factory_.GetWeakPtr(), |
| base::WrapRefCounted(print_job()), result)); |
| } |
| |
| void PrintJobWorkerOop::SendEstablishPrintingContext() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(ShouldPrintJobOop()); |
| |
| 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_); |
| } |
| |
| printing_context_id_ = service_mgr.EstablishPrintingContext( |
| *service_manager_client_id_, device_name_ |
| #if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG) |
| , |
| /*parent_view=*/nullptr |
| #endif |
| ); |
| } |
| |
| void PrintJobWorkerOop::SendStartPrinting(const std::string& device_name, |
| const std::u16string& document_name) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(ShouldPrintJobOop()); |
| |
| // 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(); |
| |
| if (!printing_context_id_.has_value()) { |
| // Need to establish a persistent printing context to use during printing. |
| SendEstablishPrintingContext(); |
| } |
| |
| #if !BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG) |
| std::optional<PrintSettings> settings; |
| if (print_from_system_dialog_) { |
| settings = document_oop_->settings(); |
| } |
| #endif |
| service_mgr.StartPrinting( |
| *service_manager_client_id_, device_name, *printing_context_id_, |
| document_cookie, document_name_, |
| #if !BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG) |
| settings, |
| #endif |
| 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(); |
| if (print_cancel_requested_) { |
| VLOG(1) << "Dropping page " << page_index << " of document " |
| << document_cookie << " to `" << device_name_ |
| << "` because job was canceled"; |
| return; |
| } |
| |
| VLOG(1) << "Sending page " << page_index << " of document " << document_cookie |
| << " to `" << device_name_ << "` for printing"; |
| PrintBackendServiceManager& service_mgr = |
| PrintBackendServiceManager::GetInstance(); |
| service_mgr.RenderPrintedPage( |
| *service_manager_client_id_, 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( |
| *service_manager_client_id_, 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(*service_manager_client_id_, device_name_, |
| document_cookie, |
| base::BindOnce(&PrintJobWorkerOop::OnDidDocumentDone, |
| ui_weak_factory_.GetWeakPtr(), |
| printing_context()->job_id())); |
| } |
| |
| void PrintJobWorkerOop::SendCancel(base::OnceClosure on_did_cancel_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 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(*service_manager_client_id_, device_name_, |
| document_oop_->cookie(), |
| std::move(on_did_cancel_callback)); |
| } |
| |
| } // namespace printing |