File: cups_print_job_manager_impl.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (546 lines) | stat: -rw-r--r-- 20,322 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <cups/cups.h>

#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/printing/cups_print_job.h"
#include "chrome/browser/ash/printing/cups_print_job_manager.h"
#include "chrome/browser/ash/printing/cups_print_job_manager_utils.h"
#include "chrome/browser/ash/printing/cups_printers_manager.h"
#include "chrome/browser/ash/printing/cups_printers_manager_factory.h"
#include "chrome/browser/ash/printing/history/print_job_info.pb.h"
#include "chrome/browser/ash/printing/history/print_job_info_proto_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/printing/cups_wrapper.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_job_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_factory.h"
#include "chromeos/printing/printing_constants.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "printing/printed_document.h"
#include "printing/printing_utils.h"
#include "ui/base/l10n/l10n_util.h"

namespace ash {

namespace {

using ::chromeos::CupsWrapper;
using StatusReason = crosapi::mojom::StatusReason::Reason;

// The rate at which we will poll CUPS for print job updates.
constexpr base::TimeDelta kPollRate = base::Milliseconds(1000);

// Threshold for giving up on communicating with CUPS.
const int kRetryMax = 6;

// 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
  CLIENT_UNAUTHORIZED = 6,  // cancelled due to client unauthorized
  RESULT_MAX
};

// Holds the print job data for recording to metrics upon print job completion.
struct PrinterMetrics {
  bool printer_manually_selected;
  std::optional<StatusReason> printer_status_reason;
};

// Returns the appropriate JobResultForHistogram for a given |state|.  Only
// FINISHED and PRINTER_CANCEL are derived from CupsPrintJob::State.
JobResultForHistogram ResultForHistogram(CupsPrintJob::State state) {
  switch (state) {
    case CupsPrintJob::State::STATE_DOCUMENT_DONE:
      return FINISHED;
    case CupsPrintJob::State::STATE_CANCELLED:
      return PRINTER_CANCEL;
    default:
      break;
  }

  return UNKNOWN;
}

void RecordJobResult(JobResultForHistogram result,
                     bool affected_by_ipp_usb_migration) {
  UMA_HISTOGRAM_ENUMERATION("Printing.CUPS.JobResult", result, RESULT_MAX);
  if (affected_by_ipp_usb_migration) {
    base::UmaHistogramEnumeration(
        "Printing.CUPS.JobResultForUsbPrintersWithIppAndPpd", result,
        RESULT_MAX);
  }
}

}  // namespace

class CupsPrintJobManagerImpl : public CupsPrintJobManager {
 public:
  explicit CupsPrintJobManagerImpl(Profile* profile)
      : CupsPrintJobManager(profile),
        cups_wrapper_(CupsWrapper::Create()),
        weak_ptr_factory_(this) {
    // NOTE: base::Unretained(this) is safe here because this object owns
    // |subscription_| and the callback won't be invoked after |subscription_|
    // is destroyed.
    subscription_ = g_browser_process->print_job_manager()->AddDocDoneCallback(
        base::BindRepeating(&CupsPrintJobManagerImpl::OnDocDone,
                            base::Unretained(this)));
    timer_.SetTaskRunner(content::GetUIThreadTaskRunner({}));
  }

  CupsPrintJobManagerImpl(const CupsPrintJobManagerImpl&) = delete;
  CupsPrintJobManagerImpl& operator=(const CupsPrintJobManagerImpl&) = delete;

  ~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";
  }

  bool ResumePrintJob(CupsPrintJob* job) override {
    NOTREACHED() << "Resume printer is not implemented";
  }

  void OnDocDone(::printing::PrintJob* job,
                 ::printing::PrintedDocument* document,
                 int job_id) {
    // Store the printer data for metrics to be recorded upon print job status
    // updates.
    const std::string printer_id =
        base::UTF16ToUTF8(document->settings().device_name());
    const std::string key = CupsPrintJob::CreateUniqueId(printer_id, job_id);
    DCHECK(!printer_metrics_cache_.contains(key));
    printer_metrics_cache_.emplace(
        key, PrinterMetrics{job->settings().printer_manually_selected(),
                            job->settings().printer_status_reason()});

    // This event occurs after the print job has been successfully sent to the
    // spooler which is when we begin tracking the print queue.
    DCHECK(document);
    std::u16string title = ::printing::SimplifyDocumentTitle(document->name());
    if (title.empty()) {
      title = ::printing::SimplifyDocumentTitle(
          l10n_util::GetStringUTF16(IDS_DEFAULT_PRINT_DOCUMENT_TITLE));
    }

    // Calculate page total for given document to ensure UI displays the correct
    // count when document has copies.
    const int total_page_number = CalculatePrintJobTotalPages(document);
    CreatePrintJob(printer_id, base::UTF16ToUTF8(title), job_id,
                   total_page_number, job->source(), job->source_id(),
                   PrintSettingsToProto(document->settings()));
  }

  // Begin monitoring a print job for a given |printer_id| with the given
  // |title| with the pages |total_page_number|.
  bool CreatePrintJob(const std::string& printer_id,
                      const std::string& title,
                      uint32_t job_id,
                      int total_page_number,
                      ::printing::PrintJob::Source source,
                      const std::string& source_id,
                      const printing::proto::PrintSettings& settings) override {
    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;
    }

    std::optional<chromeos::Printer> printer = manager->GetPrinter(printer_id);
    if (!printer) {
      LOG(WARNING)
          << "Printer was removed while job was in progress.  It cannot "
             "be tracked";
      return false;
    }

    // Record print job with scalable IPH framework.
    scalable_iph::ScalableIph* scalable_iph =
        ScalableIphFactory::GetForBrowserContext(profile);
    if (scalable_iph) {
      scalable_iph->RecordEvent(
          scalable_iph::ScalableIph::Event::kPrintJobCreated);
    }

    // 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;
  }

 private:
  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 monitoring jobs after we cancel them.  The user no longer cares.
    const std::string unique_id = job->GetUniqueId();
    jobs_.erase(unique_id);
    printer_metrics_cache_.erase(unique_id);

    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());
        }

        const bool affected_by_ipp_usb_migration =
            print_job->printer().AffectedByIppUsbMigration();
        if (print_job->error_code() ==
            chromeos::PrinterErrorCode::CLIENT_UNAUTHORIZED) {
          // Job needs to be forcibly cancelled, CUPS will keep the job in held
          // and the job cannot be resumed in chromeos.
          FinishPrintJob(print_job);
          RecordJobResult(CLIENT_UNAUTHORIZED, affected_by_ipp_usb_migration);
        } else if (print_job->IsExpired()) {
          // Job needs to be forcibly cancelled.
          RecordJobResult(TIMEOUT_CANCEL, affected_by_ipp_usb_migration);
          FinishPrintJob(print_job);
          // Beware, print_job was removed from jobs_ and
          // deleted.
        } else if (print_job->PipelineDead()) {
          RecordJobResult(FILTER_FAILED, affected_by_ipp_usb_migration);
          FinishPrintJob(print_job);
        } else if (print_job->IsJobFinished()) {
          // Cleanup completed jobs.
          VLOG(1) << "Removing Job " << print_job->document_title();
          RecordJobResult(ResultForHistogram(print_job->state()),
                          affected_by_ipp_usb_migration);
          jobs_.erase(entry);
          printer_metrics_cache_.erase(key);
        } 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.
      CupsPrintJob* job = entry.second.get();
      RecordJobResult(LOST, job->printer().AffectedByIppUsbMigration());
      job->set_state(CupsPrintJob::State::STATE_FAILED);
      NotifyJobStateUpdate(job->GetWeakPtr());
    }

    jobs_.clear();
    printer_metrics_cache_.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 CupsPrintJob::State::STATE_NONE:
        // CupsPrintJob::State does not require notification.
        break;
      case CupsPrintJob::State::STATE_WAITING:
        NotifyJobUpdated(job);
        break;
      case CupsPrintJob::State::STATE_STARTED:
        NotifyJobStarted(job);
        break;
      case CupsPrintJob::State::STATE_PAGE_DONE:
        NotifyJobUpdated(job);
        break;
      case CupsPrintJob::State::STATE_RESUMED:
        NotifyJobResumed(job);
        break;
      case CupsPrintJob::State::STATE_SUSPENDED:
        NotifyJobSuspended(job);
        break;
      case CupsPrintJob::State::STATE_CANCELLED:
        NotifyJobCanceled(job);
        break;
      case CupsPrintJob::State::STATE_FAILED:
        NotifyJobFailed(job);
        break;
      case CupsPrintJob::State::STATE_DOCUMENT_DONE:
        NotifyJobDone(job);
        break;
      case CupsPrintJob::State::STATE_ERROR:
        NotifyJobUpdated(job);
        break;
    }

    RecordPrinterMetricIfJobComplete(job);
  }

  // Only record metrics for print jobs with states that denote success complete
  // or an error status.
  void RecordPrinterMetricIfJobComplete(base::WeakPtr<CupsPrintJob> job) {
    const std::string unique_id = job->GetUniqueId();
    auto iter = printer_metrics_cache_.find(unique_id);
    if (iter == printer_metrics_cache_.end()) {
      return;
    }

    bool print_job_success;
    switch (job->state()) {
      // Consider these print job statuses as "in progress" jobs so don't record
      // metrics yet.
      case CupsPrintJob::State::STATE_NONE:
      case CupsPrintJob::State::STATE_WAITING:
      case CupsPrintJob::State::STATE_STARTED:
      case CupsPrintJob::State::STATE_PAGE_DONE:
      case CupsPrintJob::State::STATE_RESUMED:
        return;
      // The set of states for a print job considered "failed" for metrics
      // recording.
      case CupsPrintJob::State::STATE_SUSPENDED:
      case CupsPrintJob::State::STATE_CANCELLED:
      case CupsPrintJob::State::STATE_FAILED:
      case CupsPrintJob::State::STATE_ERROR:
        print_job_success = false;
        break;
      case CupsPrintJob::State::STATE_DOCUMENT_DONE:
        print_job_success = true;
        break;
    }

    // Record PrintAttemptOutcome.
    const PrinterMetrics& metrics = iter->second;
    chromeos::PrintAttemptOutcome print_attempt_outcome;
    if (print_job_success && metrics.printer_manually_selected) {
      print_attempt_outcome = chromeos::PrintAttemptOutcome::
          kPrintJobSuccessManuallySelectedPrinter;
    } else if (print_job_success && !metrics.printer_manually_selected) {
      print_attempt_outcome =
          chromeos::PrintAttemptOutcome::kPrintJobSuccessInitialPrinter;
    } else if (!print_job_success && metrics.printer_manually_selected) {
      print_attempt_outcome =
          chromeos::PrintAttemptOutcome::kPrintJobFailManuallySelectedPrinter;
    } else {
      print_attempt_outcome =
          chromeos::PrintAttemptOutcome::kPrintJobFailInitialPrinter;
    }
    base::UmaHistogramEnumeration("PrintPreview.PrintAttemptOutcome",
                                  print_attempt_outcome);

    // Record printer status print job success.
    if (metrics.printer_status_reason.has_value()) {
      const std::string histogram_name = base::StrCat(
          {"PrintPreview.PrinterStatus.",
           GetPrinterStatusHistogramName(metrics.printer_status_reason.value()),
           ".PrintJobSuccess"});
      base::UmaHistogramBoolean(histogram_name, print_job_success);
    }

    printer_metrics_cache_.erase(unique_id);
  }

  // Maps the printer status reason to the variant names used for the
  // "PrintPreview.PrinterStatus.{StatusReason}.PrintJobSuccess" histogram.
  std::string GetPrinterStatusHistogramName(
      StatusReason printer_status_reason) {
    switch (printer_status_reason) {
      case StatusReason::kUnknownReason:
        return "UnknownReason";
      case StatusReason::kDeviceError:
        return "DeviceError";
      case StatusReason::kDoorOpen:
        return "DoorOpen";
      case StatusReason::kLowOnInk:
        return "LowOnInk";
      case StatusReason::kLowOnPaper:
        return "LowOnPaper";
      case StatusReason::kNoError:
        return "NoError";
      case StatusReason::kOutOfInk:
        return "OutOfInk";
      case StatusReason::kOutOfPaper:
        return "OutOfPaper";
      case StatusReason::kOutputAreaAlmostFull:
        return "OutputAreaAlmostFull";
      case StatusReason::kOutputFull:
        return "OutputFull";
      case StatusReason::kPaperJam:
        return "PaperJam";
      case StatusReason::kPaused:
        return "Paused";
      case StatusReason::kPrinterQueueFull:
        return "PrinterQueueFull";
      case StatusReason::kPrinterUnreachable:
        return "PrinterUnreachable";
      case StatusReason::kStopped:
        return "Stopped";
      case StatusReason::kTrayMissing:
        return "TrayMissing";
      case StatusReason::kExpiredCertificate:
        return "ExpiredCertificate";
    }
  }

  // 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;

  // Stores the desired printer metrics in a map keyed by the CupsPrintJob
  // `unique_id`. Once the corresponding print job either fails or completes,
  // record the metrics entry to histograms and remove it from the map.
  base::flat_map<std::string, PrinterMetrics> printer_metrics_cache_;

  base::RepeatingTimer timer_;
  std::unique_ptr<CupsWrapper> cups_wrapper_;
  ::printing::PrintJobManager::DocDoneCallbackList::Subscription subscription_;
  base::WeakPtrFactory<CupsPrintJobManagerImpl> weak_ptr_factory_;
};

// static
std::unique_ptr<CupsPrintJobManager> CupsPrintJobManager::CreateInstance(
    Profile* profile) {
  return std::make_unique<CupsPrintJobManagerImpl>(profile);
}

}  // namespace ash