// assembleJob prepares for printing a job by fetching the job's ticket and payload. // // The caller is responsible to remove the returned file. // // Errors are returned as a string (last return value), for reporting // to GCP and local log. func (gcp *GoogleCloudPrint) assembleJob(job *Job) (*cdd.CloudJobTicket, string, string, *cdd.PrintJobStateDiff) { ticket, err := gcp.Ticket(job.GCPJobID) if err != nil { return nil, "", fmt.Sprintf("Failed to get a ticket: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseInvalidTicket}, }, } } file, err := ioutil.TempFile("", "cups-connector-gcp-") if err != nil { return nil, "", fmt.Sprintf("Failed to create a temporary file: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseOther}, }, } } gcp.downloadSemaphore.Acquire() t := time.Now() // Do not check err until semaphore is released and timer is stopped. err = gcp.Download(file, job.FileURL) dt := time.Since(t) gcp.downloadSemaphore.Release() if err != nil { // Clean up this temporary file so the caller doesn't need extra logic. os.Remove(file.Name()) return nil, "", fmt.Sprintf("Failed to download data: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseDownloadFailure}, }, } } log.InfoJobf(job.GCPJobID, "Downloaded in %s", dt.String()) defer file.Close() log.DebugJobf(job.GCPJobID, "Assembled with file %s: %+v", file.Name(), ticket.Print.Color) return ticket, file.Name(), "", &cdd.PrintJobStateDiff{} }
// processJob performs these steps: // // 1) Assembles the job resources (printer, ticket, data) // 2) Creates a new job in CUPS. // 3) Follows up with the job state until done or error. // 4) Deletes temporary file. // // Nothing is returned; intended for use as goroutine. func (gcp *GoogleCloudPrint) processJob(job *Job, printer *lib.Printer, reportJobFailed func()) { log.InfoJobf(job.GCPJobID, "Received from cloud") ticket, filename, message, state := gcp.assembleJob(job) if message != "" { reportJobFailed() log.ErrorJob(job.GCPJobID, message) if err := gcp.Control(job.GCPJobID, state); err != nil { log.ErrorJob(job.GCPJobID, err) } return } gcp.jobs <- &lib.Job{ NativePrinterName: printer.Name, Filename: filename, Title: job.Title, User: job.OwnerID, JobID: job.GCPJobID, Ticket: ticket, UpdateJob: gcp.Control, } }
// printJob prints a new job to a CUPS printer, then polls the CUPS job state // and updates the GCP/Privet job state. then returns when the job state is DONE // or ABORTED. // // All errors are reported and logged from inside this function. func (pm *PrinterManager) printJob(cupsPrinterName, filename, title, user, jobID string, ticket *cdd.CloudJobTicket, updateJob func(string, cdd.PrintJobStateDiff) error) { defer os.Remove(filename) if !pm.addInFlightJob(jobID) { // This print job was already received. We probably received it // again because the first instance is still QUEUED (ie not // IN_PROGRESS). That's OK, just throw away the second instance. return } defer pm.deleteInFlightJob(jobID) if !pm.jobFullUsername { user = strings.Split(user, "@")[0] } printer, exists := pm.printers.GetByCUPSName(cupsPrinterName) if !exists { pm.incrementJobsProcessed(false) state := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, ServiceActionCause: &cdd.ServiceActionCause{ErrorCode: cdd.ServiceActionCausePrinterDeleted}, }, } if err := updateJob(jobID, state); err != nil { log.ErrorJob(jobID, err) } return } cupsJobID, err := pm.cups.Print(&printer, filename, title, user, jobID, ticket) if err != nil { pm.incrementJobsProcessed(false) log.ErrorJobf(jobID, "Failed to submit to CUPS: %s", err) state := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCausePrintFailure}, }, } if err := updateJob(jobID, state); err != nil { log.ErrorJob(jobID, err) } return } log.InfoJobf(jobID, "Submitted as CUPS job %d", cupsJobID) var state cdd.PrintJobStateDiff ticker := time.NewTicker(time.Second) defer ticker.Stop() for _ = range ticker.C { cupsState, err := pm.cups.GetJobState(cupsJobID) if err != nil { log.WarningJobf(jobID, "Failed to get state of CUPS job %d: %s", cupsJobID, err) state = cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseOther}, }, PagesPrinted: state.PagesPrinted, } if err := updateJob(jobID, state); err != nil { log.ErrorJob(jobID, err) } pm.incrementJobsProcessed(false) return } if !reflect.DeepEqual(cupsState, state) { state = cupsState if err = updateJob(jobID, state); err != nil { log.ErrorJob(jobID, err) } log.InfoJobf(jobID, "State: %s", state.State.Type) } if state.State.Type != cdd.JobStateInProgress { if state.State.Type == cdd.JobStateDone { pm.incrementJobsProcessed(true) } else { pm.incrementJobsProcessed(false) } return } } }