blob: c10c1a7f52aeff617b508555a05661579b159449 [file] [log] [blame]
// Copyright 2017 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/chromeos/printing/cups_print_job_manager.h"
#include <cups/cups.h>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task_runner_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/timer/timer.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/chromeos/printing/cups_print_job.h"
#include "chrome/browser/chromeos/printing/cups_printers_manager.h"
#include "chrome/browser/chromeos/printing/cups_printers_manager_factory.h"
#include "chrome/browser/chromeos/printing/cups_wrapper.h"
#include "chrome/browser/chromeos/printing/history/print_job_info.pb.h"
#include "chrome/browser/chromeos/printing/history/print_job_info_proto_conversions.h"
#include "chrome/browser/chromeos/printing/printer_error_codes.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_registrar.h"
#include "content/public/browser/notification_service.h"
#include "printing/printed_document.h"
#include "printing/printing_utils.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
// The rate at which we will poll CUPS for print job updates.
constexpr base::TimeDelta kPollRate = base::TimeDelta::FromMilliseconds(1000);
// Threshold for giving up on communicating with CUPS.
const int kRetryMax = 6;
// job state reason values
const char kJobCompletedWithErrors[] = "job-completed-with-errors";
using State = chromeos::CupsPrintJob::State;
using PrinterErrorCode = chromeos::PrinterErrorCode;
using PrinterReason = printing::PrinterStatus::PrinterReason;
// Enumeration of print job results for histograms. Do not modify!
enum JobResultForHistogram {
UNKNOWN = 0, // unidentified result
FINISHED = 1, // successful completion of job
TIMEOUT_CANCEL = 2, // cancelled due to timeout
PRINTER_CANCEL = 3, // cancelled by printer
LOST = 4, // final state never received
FILTER_FAILED = 5, // filter failed
RESULT_MAX
};
// Returns the appropriate JobResultForHistogram for a given |state|. Only
// FINISHED and PRINTER_CANCEL are derived from CupsPrintJob::State.
JobResultForHistogram ResultForHistogram(State state) {
switch (state) {
case State::STATE_DOCUMENT_DONE:
return FINISHED;
case State::STATE_CANCELLED:
return PRINTER_CANCEL;
default:
break;
}
return UNKNOWN;
}
void RecordJobResult(JobResultForHistogram result) {
UMA_HISTOGRAM_ENUMERATION("Printing.CUPS.JobResult", result, RESULT_MAX);
}
// Returns the equivalient CupsPrintJob#State from a CupsJob#JobState.
State ConvertState(printing::CupsJob::JobState state) {
switch (state) {
case printing::CupsJob::PENDING:
return State::STATE_WAITING;
case printing::CupsJob::HELD:
return State::STATE_SUSPENDED;
case printing::CupsJob::PROCESSING:
return State::STATE_STARTED;
case printing::CupsJob::CANCELED:
return State::STATE_CANCELLED;
case printing::CupsJob::COMPLETED:
return State::STATE_DOCUMENT_DONE;
case printing::CupsJob::STOPPED:
return State::STATE_SUSPENDED;
case printing::CupsJob::ABORTED:
return State::STATE_FAILED;
case printing::CupsJob::UNKNOWN:
break;
}
NOTREACHED();
return State::STATE_NONE;
}
// Returns true if |job|.state_reasons contains |reason|
bool JobContainsReason(const ::printing::CupsJob& job,
base::StringPiece reason) {
return base::Contains(job.state_reasons, reason);
}
// Update the current printed page. Returns true of the page has been updated.
bool UpdateCurrentPage(const printing::CupsJob& job,
chromeos::CupsPrintJob* print_job) {
bool pages_updated = false;
if (job.current_pages <= 0) {
print_job->set_printed_page_number(0);
print_job->set_state(State::STATE_STARTED);
} else {
pages_updated = job.current_pages != print_job->printed_page_number();
print_job->set_printed_page_number(job.current_pages);
print_job->set_state(State::STATE_PAGE_DONE);
}
return pages_updated;
}
// Updates the state of a print job based on |printer_status| and |job|.
// Returns true if observers need to be notified of an update.
bool UpdatePrintJob(const ::printing::PrinterStatus& printer_status,
const ::printing::CupsJob& job,
chromeos::CupsPrintJob* print_job) {
DCHECK_EQ(job.id, print_job->job_id());
State old_state = print_job->state();
bool pages_updated = false;
switch (job.state) {
case ::printing::CupsJob::PROCESSING:
pages_updated = UpdateCurrentPage(job, print_job);
if (chromeos::PrinterErrorCodeFromPrinterStatusReasons(printer_status) !=
PrinterErrorCode::NO_ERROR) {
print_job->set_error_code(
chromeos::PrinterErrorCodeFromPrinterStatusReasons(printer_status));
print_job->set_state(State::STATE_ERROR);
} else {
print_job->set_state(State::STATE_STARTED);
print_job->set_error_code(PrinterErrorCode::NO_ERROR);
}
break;
case ::printing::CupsJob::COMPLETED:
DCHECK_GE(job.current_pages, print_job->total_page_number());
print_job->set_error_code(PrinterErrorCode::NO_ERROR);
print_job->set_state(State::STATE_DOCUMENT_DONE);
break;
case ::printing::CupsJob::STOPPED:
// If cups job STOPPED but with filter failure, treat as ERROR
if (JobContainsReason(job, kJobCompletedWithErrors)) {
print_job->set_error_code(PrinterErrorCode::FILTER_FAILED);
print_job->set_state(State::STATE_FAILED);
} else {
print_job->set_error_code(PrinterErrorCode::NO_ERROR);
print_job->set_state(ConvertState(job.state));
}
break;
case ::printing::CupsJob::ABORTED:
case ::printing::CupsJob::CANCELED:
print_job->set_error_code(
chromeos::PrinterErrorCodeFromPrinterStatusReasons(printer_status));
FALLTHROUGH;
default:
print_job->set_state(ConvertState(job.state));
break;
}
return print_job->state() != old_state || pages_updated;
}
} // namespace
namespace chromeos {
class CupsPrintJobManagerImpl : public CupsPrintJobManager,
public content::NotificationObserver {
public:
explicit CupsPrintJobManagerImpl(Profile* profile)
: CupsPrintJobManager(profile),
cups_wrapper_(CupsWrapper::Create()),
weak_ptr_factory_(this) {
timer_.SetTaskRunner(content::GetUIThreadTaskRunner({}));
registrar_.Add(this, chrome::NOTIFICATION_PRINT_JOB_EVENT,
content::NotificationService::AllSources());
}
~CupsPrintJobManagerImpl() override = default;
// CupsPrintJobManager overrides:
// Must be run from the UI thread.
void CancelPrintJob(CupsPrintJob* job) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
job->set_state(CupsPrintJob::State::STATE_CANCELLED);
NotifyJobCanceled(job->GetWeakPtr());
// Ideally we should wait for IPP response.
FinishPrintJob(job);
}
bool SuspendPrintJob(CupsPrintJob* job) override {
NOTREACHED() << "Pause printer is not implemented";
return false;
}
bool ResumePrintJob(CupsPrintJob* job) override {
NOTREACHED() << "Resume printer is not implemented";
return false;
}
// NotificationObserver overrides:
void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) override {
DCHECK_EQ(chrome::NOTIFICATION_PRINT_JOB_EVENT, type);
content::Details<::printing::JobEventDetails> job_details(details);
content::Source<::printing::PrintJob> job(source);
// DOC_DONE occurs after the print job has been successfully sent to the
// spooler which is when we begin tracking the print queue.
if (job_details->type() == ::printing::JobEventDetails::DOC_DONE) {
const ::printing::PrintedDocument* document = job_details->document();
DCHECK(document);
std::u16string title =
::printing::SimplifyDocumentTitle(document->name());
if (title.empty()) {
title = ::printing::SimplifyDocumentTitle(
l10n_util::GetStringUTF16(IDS_DEFAULT_PRINT_DOCUMENT_TITLE));
}
CreatePrintJob(base::UTF16ToUTF8(document->settings().device_name()),
base::UTF16ToUTF8(title), job_details->job_id(),
document->page_count(), job->source(), job->source_id(),
PrintSettingsToProto(document->settings()));
}
}
private:
// Begin monitoring a print job for a given |printer_name| with the given
// |title| with the pages |total_page_number|.
bool CreatePrintJob(const std::string& printer_name,
const std::string& title,
int job_id,
int total_page_number,
::printing::PrintJob::Source source,
const std::string& source_id,
const printing::proto::PrintSettings& settings) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
Profile* profile = ProfileManager::GetPrimaryUserProfile();
if (!profile) {
LOG(WARNING) << "Cannot find printer without a valid profile.";
return false;
}
auto* manager = CupsPrintersManagerFactory::GetForBrowserContext(profile);
if (!manager) {
LOG(WARNING)
<< "CupsPrintersManager could not be found for the current profile.";
return false;
}
base::Optional<Printer> printer = manager->GetPrinter(printer_name);
if (!printer) {
LOG(WARNING)
<< "Printer was removed while job was in progress. It cannot "
"be tracked";
return false;
}
// Create a new print job.
auto cpj = std::make_unique<CupsPrintJob>(*printer, job_id, title,
total_page_number, source,
source_id, settings);
std::string key = cpj->GetUniqueId();
jobs_[key] = std::move(cpj);
CupsPrintJob* job = jobs_[key].get();
NotifyJobCreated(job->GetWeakPtr());
// Always start jobs in the waiting state.
job->set_state(CupsPrintJob::State::STATE_WAITING);
NotifyJobUpdated(job->GetWeakPtr());
// Run a query now.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&CupsPrintJobManagerImpl::PostQuery,
weak_ptr_factory_.GetWeakPtr()));
// Start the timer for ongoing queries.
ScheduleQuery();
return true;
}
void FinishPrintJob(CupsPrintJob* job) {
// Copy job_id and printer_id. |job| is about to be freed.
const int job_id = job->job_id();
const std::string printer_id = job->printer().id();
// Stop montioring jobs after we cancel them. The user no longer cares.
jobs_.erase(job->GetUniqueId());
cups_wrapper_->CancelJob(printer_id, job_id);
}
// Schedule a query of CUPS for print job status with a delay of |delay|.
void ScheduleQuery(int attempt_count = 1) {
timer_.Start(FROM_HERE, kPollRate * attempt_count,
base::BindRepeating(&CupsPrintJobManagerImpl::PostQuery,
weak_ptr_factory_.GetWeakPtr()));
}
void PostQuery() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// The set of active printers is expected to be small.
std::set<std::string> printer_ids;
for (const auto& entry : jobs_) {
printer_ids.insert(entry.second->printer().id());
}
std::vector<std::string> ids{printer_ids.begin(), printer_ids.end()};
cups_wrapper_->QueryCupsPrintJobs(
ids, base::BindOnce(&CupsPrintJobManagerImpl::UpdateJobs,
weak_ptr_factory_.GetWeakPtr()));
}
// Process jobs from CUPS and perform notifications.
// Use job information to update local job states. Previously completed jobs
// could be in |jobs| but those are ignored as we will not emit updates for
// them after they are completed.
void UpdateJobs(std::unique_ptr<CupsWrapper::QueryResult> result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// If the query failed, either retry or purge.
if (!result->success) {
retry_count_++;
LOG(WARNING) << "Failed to query CUPS for queue status. Schedule retry ("
<< retry_count_ << ")";
if (retry_count_ > kRetryMax) {
LOG(ERROR) << "CUPS is unreachable. Giving up on all jobs.";
timer_.Stop();
PurgeJobs();
} else {
// Backoff the polling frequency. Give CUPS a chance to recover.
DCHECK_GE(1, retry_count_);
ScheduleQuery(retry_count_);
}
return;
}
// A query has completed. Reset retry counter.
retry_count_ = 0;
std::vector<std::string> active_jobs;
for (const auto& queue : result->queues) {
for (auto& job : queue.jobs) {
std::string key = CupsPrintJob::CreateUniqueId(job.printer_id, job.id);
const auto& entry = jobs_.find(key);
if (entry == jobs_.end())
continue;
CupsPrintJob* print_job = entry->second.get();
if (UpdatePrintJob(queue.printer_status, job, print_job)) {
// The state of the job changed, notify observers.
NotifyJobStateUpdate(print_job->GetWeakPtr());
}
if (print_job->IsExpired()) {
// Job needs to be forcibly cancelled.
RecordJobResult(TIMEOUT_CANCEL);
FinishPrintJob(print_job);
// Beware, print_job was removed from jobs_ and
// deleted.
} else if (print_job->PipelineDead()) {
RecordJobResult(FILTER_FAILED);
FinishPrintJob(print_job);
} else if (print_job->IsJobFinished()) {
// Cleanup completed jobs.
VLOG(1) << "Removing Job " << print_job->document_title();
RecordJobResult(ResultForHistogram(print_job->state()));
jobs_.erase(entry);
} else {
active_jobs.push_back(key);
}
}
}
if (active_jobs.empty()) {
// CUPS has stopped reporting jobs. Stop polling.
timer_.Stop();
if (!jobs_.empty()) {
// We're tracking jobs that we didn't receive an update for. Something
// bad has happened.
LOG(ERROR) << "Lost track of (" << jobs_.size() << ") jobs";
PurgeJobs();
}
}
}
// Mark remaining jobs as errors and remove active jobs.
void PurgeJobs() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
for (const auto& entry : jobs_) {
// Declare all lost jobs errors.
RecordJobResult(LOST);
CupsPrintJob* job = entry.second.get();
job->set_state(CupsPrintJob::State::STATE_FAILED);
NotifyJobStateUpdate(job->GetWeakPtr());
}
jobs_.clear();
}
// Notify observers that a state update has occurred for |job|.
void NotifyJobStateUpdate(base::WeakPtr<CupsPrintJob> job) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!job)
return;
switch (job->state()) {
case State::STATE_NONE:
// State does not require notification.
break;
case State::STATE_WAITING:
NotifyJobUpdated(job);
break;
case State::STATE_STARTED:
NotifyJobStarted(job);
break;
case State::STATE_PAGE_DONE:
NotifyJobUpdated(job);
break;
case State::STATE_RESUMED:
NotifyJobResumed(job);
break;
case State::STATE_SUSPENDED:
NotifyJobSuspended(job);
break;
case State::STATE_CANCELLED:
NotifyJobCanceled(job);
break;
case State::STATE_FAILED:
NotifyJobFailed(job);
break;
case State::STATE_DOCUMENT_DONE:
NotifyJobDone(job);
break;
case State::STATE_ERROR:
NotifyJobUpdated(job);
break;
}
}
// Ongoing print jobs.
std::map<std::string, std::unique_ptr<CupsPrintJob>> jobs_;
// Records the number of consecutive times the GetJobs query has failed.
int retry_count_ = 0;
base::RepeatingTimer timer_;
content::NotificationRegistrar registrar_;
std::unique_ptr<CupsWrapper> cups_wrapper_;
base::WeakPtrFactory<CupsPrintJobManagerImpl> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(CupsPrintJobManagerImpl);
};
// static
CupsPrintJobManager* CupsPrintJobManager::CreateInstance(Profile* profile) {
return new CupsPrintJobManagerImpl(profile);
}
} // namespace chromeos