| // 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/ash/printing/cups_print_job_manager_utils.h" |
| |
| #include <algorithm> |
| #include <string_view> |
| |
| #include "base/check_op.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/printing/cups_print_job.h" |
| #include "chrome/browser/chromeos/printing/printer_error_codes.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "printing/backend/cups_jobs.h" |
| #include "printing/printed_document.h" |
| #include "printing/printer_status.h" |
| #include "third_party/abseil-cpp/absl/container/flat_hash_map.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using ::chromeos::PrinterErrorCode; |
| using ::chromeos::PrinterErrorCodeFromPrinterStatusReasons; |
| |
| // The amount of time elapsed from print job creation before a timeout is |
| // acknowledged. CUPS has a timeout of ~25s. |
| constexpr base::TimeDelta kMinElaspedPrintJobTimeout = base::Seconds(30); |
| |
| // Returns the equivalent CupsPrintJob#State from a CupsJob#JobState. |
| CupsPrintJob::State ConvertState(::printing::CupsJob::JobState state) { |
| switch (state) { |
| case ::printing::CupsJob::PENDING: |
| return CupsPrintJob::State::STATE_WAITING; |
| case ::printing::CupsJob::HELD: |
| return CupsPrintJob::State::STATE_SUSPENDED; |
| case ::printing::CupsJob::PROCESSING: |
| return CupsPrintJob::State::STATE_STARTED; |
| case ::printing::CupsJob::CANCELED: |
| return CupsPrintJob::State::STATE_CANCELLED; |
| case ::printing::CupsJob::COMPLETED: |
| return CupsPrintJob::State::STATE_DOCUMENT_DONE; |
| case ::printing::CupsJob::STOPPED: |
| return CupsPrintJob::State::STATE_SUSPENDED; |
| case ::printing::CupsJob::ABORTED: |
| return CupsPrintJob::State::STATE_FAILED; |
| case ::printing::CupsJob::UNKNOWN: |
| break; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| // Returns a string description of CUPS printer-state names |
| std::string_view PrinterStateName(ipp_pstate_t state) { |
| switch (state) { |
| case IPP_PSTATE_IDLE: |
| return "idle"; |
| case IPP_PSTATE_PROCESSING: |
| return "processing"; |
| case IPP_PSTATE_STOPPED: |
| return "stopped"; |
| default: |
| return "unknown"; |
| } |
| } |
| |
| // Returns a string description of CUPS job-state names |
| std::string_view JobStateName(::printing::CupsJob::JobState state) { |
| switch (state) { |
| case ::printing::CupsJob::PENDING: |
| return "pending"; |
| case ::printing::CupsJob::HELD: |
| return "held"; |
| case ::printing::CupsJob::PROCESSING: |
| return "processing"; |
| case ::printing::CupsJob::CANCELED: |
| return "canceled"; |
| case ::printing::CupsJob::COMPLETED: |
| return "completed"; |
| case ::printing::CupsJob::STOPPED: |
| return "stopped"; |
| case ::printing::CupsJob::ABORTED: |
| return "aborted"; |
| case ::printing::CupsJob::UNKNOWN: |
| return "unknown"; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| std::string GetStateDescription(const ::printing::PrinterStatus& printer_status, |
| const ::printing::CupsJob& job) { |
| return base::StringPrintf("job=%s[%s] printer=%s[%s]", |
| JobStateName(job.state), |
| base::JoinString(job.state_reasons, ";"), |
| PrinterStateName(printer_status.state), |
| printer_status.AllReasonsAsString()); |
| } |
| |
| // Update the current printed page. Returns true of the page has been updated. |
| bool UpdateCurrentPage(const ::printing::CupsJob& job, |
| CupsPrintJob* print_job) { |
| bool pages_updated = false; |
| if (job.current_pages <= 0 || |
| print_job->state() == CupsPrintJob::State::STATE_WAITING) { |
| print_job->set_printed_page_number(std::max(job.current_pages, 0)); |
| print_job->set_state(CupsPrintJob::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(CupsPrintJob::State::STATE_PAGE_DONE); |
| } |
| |
| return pages_updated; |
| } |
| |
| void UpdateProcessingJob(const ::printing::PrinterStatus& printer_status, |
| const ::printing::CupsJob& job, |
| CupsPrintJob* print_job, |
| bool* pages_updated) { |
| *pages_updated = UpdateCurrentPage(job, print_job); |
| |
| const PrinterErrorCode printer_error_code = |
| PrinterErrorCodeFromPrinterStatusReasons(printer_status); |
| const bool delay_print_job_timeout = |
| printer_error_code == PrinterErrorCode::PRINTER_UNREACHABLE && |
| (base::Time::Now() - print_job->creation_time() < |
| kMinElaspedPrintJobTimeout); |
| |
| if (printer_error_code != PrinterErrorCode::NO_ERROR && |
| !delay_print_job_timeout) { |
| print_job->set_error_code(printer_error_code); |
| print_job->set_state(printer_error_code == |
| PrinterErrorCode::PRINTER_UNREACHABLE |
| ? CupsPrintJob::State::STATE_FAILED |
| : CupsPrintJob::State::STATE_ERROR); |
| } else { |
| print_job->set_error_code(PrinterErrorCode::NO_ERROR); |
| } |
| } |
| |
| void UpdateCompletedJob(const ::printing::PrinterStatus& printer_status, |
| const ::printing::CupsJob& job, |
| CupsPrintJob* print_job) { |
| if (job.current_pages >= print_job->total_page_number()) { |
| print_job->set_error_code(PrinterErrorCode::NO_ERROR); |
| print_job->set_state(CupsPrintJob::State::STATE_DOCUMENT_DONE); |
| } else { |
| print_job->set_error_code( |
| PrinterErrorCodeFromPrinterStatusReasons(printer_status)); |
| print_job->set_state(CupsPrintJob::State::STATE_CANCELLED); |
| } |
| } |
| |
| void UpdateStoppedJob(const ::printing::CupsJob& job, CupsPrintJob* print_job) { |
| // If cups job STOPPED but with filter failure, treat as ERROR |
| if (job.ContainsStateReason( |
| ::printing::CupsJob::JobStateReason::kJobCompletedWithErrors)) { |
| print_job->set_error_code(PrinterErrorCode::FILTER_FAILED); |
| print_job->set_state(CupsPrintJob::State::STATE_FAILED); |
| } else { |
| print_job->set_error_code(PrinterErrorCode::NO_ERROR); |
| print_job->set_state(ConvertState(job.state)); |
| } |
| } |
| |
| void UpdateHeldJob(const ::printing::CupsJob& job, CupsPrintJob* print_job) { |
| // If cups job STOPPED but with cups held for authentication, treat as ERROR |
| if (job.ContainsStateReason( |
| ::printing::CupsJob::JobStateReason::kCupsHeldForAuthentication)) { |
| print_job->set_error_code(PrinterErrorCode::CLIENT_UNAUTHORIZED); |
| print_job->set_state(CupsPrintJob::State::STATE_FAILED); |
| } else { |
| print_job->set_error_code(PrinterErrorCode::NO_ERROR); |
| print_job->set_state(ConvertState(job.state)); |
| } |
| } |
| |
| } // namespace |
| |
| bool UpdatePrintJob(const ::printing::PrinterStatus& printer_status, |
| const ::printing::CupsJob& job, |
| CupsPrintJob* print_job) { |
| static absl::flat_hash_map<int, std::string> old_status; |
| |
| DCHECK_EQ(job.id, print_job->job_id()); |
| |
| CupsPrintJob::State old_state = print_job->state(); |
| |
| bool pages_updated = false; |
| switch (job.state) { |
| case ::printing::CupsJob::PROCESSING: |
| UpdateProcessingJob(printer_status, job, print_job, &pages_updated); |
| break; |
| case ::printing::CupsJob::COMPLETED: |
| UpdateCompletedJob(printer_status, job, print_job); |
| break; |
| case ::printing::CupsJob::STOPPED: |
| UpdateStoppedJob(job, print_job); |
| break; |
| case ::printing::CupsJob::HELD: |
| UpdateHeldJob(job, print_job); |
| break; |
| case ::printing::CupsJob::ABORTED: |
| case ::printing::CupsJob::CANCELED: |
| print_job->set_error_code( |
| PrinterErrorCodeFromPrinterStatusReasons(printer_status)); |
| [[fallthrough]]; |
| default: |
| print_job->set_state(ConvertState(job.state)); |
| break; |
| } |
| |
| // If the status has changed since the last time the job was polled, log the |
| // new status. The printer-state-reasons and job-state-reasons can change |
| // even if the job-state doesn't change, so this is based on matching the |
| // previous status message instead of looking at just the state. |
| bool updated = print_job->state() != old_state || pages_updated; |
| std::string status = base::StringPrintf( |
| "%s: job %d changed to page %d/%d with state: %s", job.printer_id, job.id, |
| print_job->printed_page_number(), print_job->total_page_number(), |
| GetStateDescription(printer_status, job)); |
| if (status != old_status[job.id]) { |
| if (updated) { |
| PRINTER_LOG(EVENT) << status; |
| } else { |
| PRINTER_LOG(DEBUG) << status; |
| } |
| old_status[job.id] = status; |
| } |
| if (job.state == ::printing::CupsJob::COMPLETED || |
| job.state == ::printing::CupsJob::CANCELED || |
| job.state == ::printing::CupsJob::ABORTED) { |
| // No need to save statuses for terminal states, since no more updates are |
| // expected. |
| old_status.erase(job.id); |
| } |
| |
| return updated; |
| } |
| |
| int CalculatePrintJobTotalPages(const ::printing::PrintedDocument* document) { |
| if (document->settings().copies() == 0) { |
| return document->page_count(); |
| } |
| |
| return document->page_count() * document->settings().copies(); |
| } |
| |
| } // namespace ash |