source: server/lib/gutenbach/server/printer.py @ 5e70cc2

no-cups
Last change on this file since 5e70cc2 was 5e70cc2, checked in by Jessica B. Hamrick <jhamrick@…>, 12 years ago

Make threading in printer.py more robust

  • Property mode set to 100644
File size: 20.8 KB
Line 
1from .errors import InvalidJobException, InvalidPrinterStateException, InvalidJobStateException
2from .job import GutenbachJob
3from gutenbach.ipp import PrinterStates as States
4import gutenbach.ipp as ipp
5import logging
6import time
7import threading
8import heapq
9import traceback
10import sys
11import tempfile
12from . import sync
13
14
15# initialize logger
16logger = logging.getLogger(__name__)
17
18class GutenbachPrinter(threading.Thread):
19
20    # for IPP
21    printer_attributes = [
22        "printer-uri-supported",
23        "uri-authentication-supported",
24        "uri-security-supported",
25        "printer-name",
26        "printer-state",
27        "printer-state-reasons",
28        "ipp-versions-supported",
29        "operations-supported",
30        "charset-configured",
31        "charset-supported",
32        "natural-language-configured",
33        "generated-natural-language-supported",
34        "document-format-default",
35        "document-format-supported",
36        "printer-is-accepting-jobs",
37        "queued-job-count",
38        "pdl-override-supported",
39        "printer-up-time",
40        "compression-supported",
41        "multiple-operation-time-out",
42        "multiple-document-jobs-supported",
43    ]
44
45    job_attributes = [
46        "job-id",
47        "job-name",
48        "job-originating-user-name",
49        "job-k-octets",
50        "job-state",
51        "job-printer-uri"
52    ]
53
54    operations = [
55        "print-job",
56        "validate-job",
57        "get-jobs",
58        "print-uri",
59        "create-job",
60        "pause-printer",
61        "resume-printer",
62        "get-printer-attributes",
63        "set-printer-attributes",
64        "cancel-job",
65        "send-document",
66        "send-uri",
67        "get-job-attributes",
68        "set-job-attributes",
69        "restart-job",
70        "promote-job"
71    ]
72       
73    def __init__(self, name, config, *args, **kwargs):
74
75        super(GutenbachPrinter, self).__init__(*args, **kwargs)
76       
77        self.name = name
78        self.config = config
79        self.time_created = int(time.time())
80
81        self.finished_jobs = []
82        self.pending_jobs = []
83        self.current_job = None
84        self.jobs = {}
85
86        self.lock = threading.RLock()
87        self._running = False
88        self.paused = False
89
90        # CUPS ignores jobs with id 0, so we have to start at 1
91        self._next_job_id = 1
92
93    @sync
94    def __repr__(self):
95        return str(self)
96
97    @sync
98    def __str__(self):
99        return "<Printer '%s'>" % self.name
100
101    def run(self):
102        self._running = True
103        while self._running:
104            try:
105                with self.lock:
106                    if self.current_job is None:
107                        self.start_job()
108                    elif self.current_job.is_done:
109                        self.complete_job()
110            except:
111                logger.fatal(traceback.format_exc())
112                sys.exit(1)
113            time.sleep(0.1)
114
115    def stop(self):
116        with self.lock:
117            self._running = False
118        if self.ident is not None and self.isAlive():
119            self.join()
120
121    ######################################################################
122    ###                          Properties                            ###
123    ######################################################################
124
125    @property
126    def uris(self):
127        uris = ["ipp://localhost:8000/printers/" + self.name,
128                "ipp://localhost/printers/" + self.name]
129        return uris
130   
131    @property
132    def uri(self):
133        return self.uris[0]
134
135    @property
136    @sync
137    def state(self):
138        if self.is_running and self.current_job is not None:
139            state = States.PROCESSING
140        elif self.is_running and len(self.pending_jobs) == 0:
141            state = States.IDLE
142        else:
143            state = States.STOPPED
144        return state
145
146    @property
147    @sync
148    def active_jobs(self):
149        jobs = self.pending_jobs[:]
150        if self.current_job is not None:
151            jobs.insert(0, self.current_job.id)
152        return jobs
153
154    @property
155    def is_running(self):
156        running = self.ident is not None and self.isAlive() and self._running
157        return running
158
159    ######################################################################
160    ###                            Methods                             ###
161    ######################################################################
162
163    @sync
164    def assert_running(self):
165        if not self.is_running:
166            raise RuntimeError, "%s not started" % str(self)
167
168    @sync
169    def start_job(self):
170        self.assert_running()
171        if not self.paused and self.current_job is None:
172            try:
173                job_id = heapq.heappop(self.pending_jobs)
174                self.current_job = self.get_job(job_id)
175                self.current_job.play()
176            except IndexError:
177                self.current_job = None
178            except InvalidJobStateException:
179                heapq.heappush(self.pending_jobs, self.current_job.id)
180                self.current_job = None
181                   
182    @sync
183    def complete_job(self):
184        self.assert_running()
185        if not self.paused and self.current_job is not None:
186            try:
187                if not self.current_job.is_done:
188                    self.current_job.stop()
189            finally:
190                self.finished_jobs.append(self.current_job.id)
191                self.current_job = None
192
193    @sync
194    def get_job(self, job_id):
195        self.assert_running()
196        if job_id not in self.jobs:
197            raise InvalidJobException(job_id)
198        return self.jobs[job_id]
199
200    ######################################################################
201    ###                        IPP Attributes                          ###
202    ######################################################################
203
204    @property
205    def printer_uri_supported(self):
206        self.assert_running()
207        return ipp.PrinterUriSupported(self.uri)
208    @printer_uri_supported.setter
209    def printer_uri_supported(self, val):
210        self.assert_running()
211        raise ipp.errors.AttributesNotSettable("printer-uri-supported")
212
213    @property
214    def uri_authentication_supported(self):
215        self.assert_running()
216        return ipp.UriAuthenticationSupported("none")
217    @uri_authentication_supported.setter
218    def uri_authentication_supported(self, val):
219        self.assert_running()
220        raise ipp.errors.AttributesNotSettable("uri-authentication-supported")
221
222    @property
223    def uri_security_supported(self):
224        self.assert_running()
225        return ipp.UriSecuritySupported("none")
226    @uri_security_supported.setter
227    def uri_security_supported(self, val):
228        self.assert_running()
229        raise ipp.errors.AttributesNotSettable("uri-security-supported")
230
231    @property
232    def printer_name(self):
233        self.assert_running()
234        return ipp.PrinterName(self.name)
235    @printer_name.setter
236    def printer_name(self, val):
237        self.assert_running()
238        raise ipp.errors.AttributesNotSettable("printer-name")
239
240    @property
241    def printer_state(self):
242        self.assert_running()
243        return ipp.PrinterState(self.state)
244    @printer_state.setter
245    def printer_state(self, val):
246        self.assert_running()
247        raise ipp.errors.AttributesNotSettable("printer-state")
248
249    @property
250    def printer_state_reasons(self):
251        self.assert_running()
252        return ipp.PrinterStateReasons("none")
253    @printer_state_reasons.setter
254    def printer_state_reasons(self, val):
255        self.assert_running()
256        raise ipp.errors.AttributesNotSettable("printer-state-reasons")
257
258    @property
259    def ipp_versions_supported(self):
260        self.assert_running()
261        return ipp.IppVersionsSupported(*self.config['ipp-versions'])
262    @ipp_versions_supported.setter
263    def ipp_versions_supported(self, val):
264        self.assert_running()
265        raise ipp.errors.AttributesNotSettable("ipp-versions-supported")
266
267    # XXX: We should query ourself for the supported operations
268    @property
269    def operations_supported(self):
270        self.assert_running()
271        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
272    @operations_supported.setter
273    def operations_supported(self, val):
274        self.assert_running()
275        raise ipp.errors.AttributesNotSettable("operations-supported")
276
277    @property
278    def charset_configured(self):
279        self.assert_running()
280        return ipp.CharsetConfigured("utf-8") # XXX
281    @charset_configured.setter
282    def charset_configured(self, val):
283        self.assert_running()
284        raise ipp.errors.AttributesNotSettable("charset-configured")
285       
286    @property
287    def charset_supported(self):
288        self.assert_running()
289        return ipp.CharsetSupported("utf-8") # XXX
290    @charset_supported.setter
291    def charset_supported(self, val):
292        self.assert_running()
293        raise ipp.errors.AttributesNotSettable("charset-supported")
294
295    @property
296    def natural_language_configured(self):
297        self.assert_running()
298        return ipp.NaturalLanguageConfigured("en-us")
299    @natural_language_configured.setter
300    def natural_language_configured(self, val):
301        self.assert_running()
302        raise ipp.errors.AttributesNotSettable("natural-language-configured")
303
304    @property
305    def generated_natural_language_supported(self):
306        self.assert_running()
307        return ipp.GeneratedNaturalLanguageSupported("en-us")
308    @generated_natural_language_supported.setter
309    def generated_natural_language_supported(self, val):
310        self.assert_running()
311        raise ipp.errors.AttributesNotSettable("generated-natural-language-supported")
312
313    @property
314    def document_format_default(self):
315        self.assert_running()
316        return ipp.DocumentFormatDefault("application/octet-stream")
317    @document_format_default.setter
318    def document_format_default(self, val):
319        self.assert_running()
320        raise ipp.errors.AttributesNotSettable("document-format-default")
321
322    @property
323    def document_format_supported(self):
324        self.assert_running()
325        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
326    @document_format_supported.setter
327    def document_format_supported(self, val):
328        self.assert_running()
329        raise ipp.errors.AttributesNotSettable("document-format-supported")
330
331    @property
332    def printer_is_accepting_jobs(self):
333        self.assert_running()
334        return ipp.PrinterIsAcceptingJobs(True)
335    @printer_is_accepting_jobs.setter
336    def printer_is_accepting_jobs(self, val):
337        self.assert_running()
338        raise ipp.errors.AttributesNotSettable("printer-is-accepting-jobs")
339
340    @property
341    def queued_job_count(self):
342        self.assert_running()
343        return ipp.QueuedJobCount(len(self.active_jobs))
344    @queued_job_count.setter
345    def queued_job_count(self, val):
346        self.assert_running()
347        raise ipp.errors.AttributesNotSettable("queued-job-count")
348
349    @property
350    def pdl_override_supported(self):
351        self.assert_running()
352        return ipp.PdlOverrideSupported("not-attempted")
353    @pdl_override_supported.setter
354    def pdl_override_supported(self, val):
355        self.assert_running()
356        raise ipp.errors.AttributesNotSettable("pdl-override-supported")
357
358    @property
359    def printer_up_time(self):
360        self.assert_running()
361        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
362    @printer_up_time.setter
363    def printer_up_time(self, val):
364        self.assert_running()
365        raise ipp.errors.AttributesNotSettable("printer-up-time")
366
367    @property
368    def compression_supported(self):
369        self.assert_running()
370        return ipp.CompressionSupported("none")
371    @compression_supported.setter
372    def compression_supported(self, val):
373        self.assert_running()
374        raise ipp.errors.AttributesNotSettable("compression-supported")
375
376    @property
377    def multiple_operation_time_out(self):
378        self.assert_running()
379        return ipp.MultipleOperationTimeOut(240)
380    @multiple_operation_time_out.setter
381    def multiple_operation_time_out(self, val):
382        self.assert_running()
383        raise ipp.errors.AttributesNotSettable("multiple-operation-time-out")
384
385    @property
386    def multiple_document_jobs_supported(self):
387        self.assert_running()
388        return ipp.MultipleDocumentJobsSupported(False)
389    @multiple_document_jobs_supported.setter
390    def multiple_document_jobs_supported(self, val):
391        self.assert_running()
392        raise ipp.errors.AttributesNotSettable("multiple-document-jobs-supported")
393
394    ######################################################################
395    ###                      Job IPP Attributes                        ###
396    ######################################################################
397
398    def job_id(self, job_id):
399        self.assert_running()
400        job = self.get_job(job_id)
401        return ipp.JobId(job.id)
402
403    def job_name(self, job_id):
404        self.assert_running()
405        job = self.get_job(job_id)
406        return ipp.JobName(job.name)
407
408    def job_originating_user_name(self, job_id):
409        self.assert_running()
410        job = self.get_job(job_id)
411        return ipp.JobOriginatingUserName(job.creator)
412
413    def job_k_octets(self, job_id):
414        self.assert_running()
415        job = self.get_job(job_id)
416        return ipp.JobKOctets(job.size)
417
418    def job_state(self, job_id):
419        self.assert_running()
420        job = self.get_job(job_id)
421        return ipp.JobState(job.state)
422
423    def job_printer_uri(self, job_id):
424        self.assert_running()
425        job = self.get_job(job_id)
426        return ipp.JobPrinterUri(self.uri)
427
428    ######################################################################
429    ###                        IPP Operations                          ###
430    ######################################################################
431
432    @sync
433    def print_job(self, document, document_name=None, document_format=None,
434                  document_natural_language=None, requesting_user_name=None,
435                  compression=None, job_name=None, job_k_octets=None):
436
437        self.assert_running()
438
439        # create the job
440        job_id = self.create_job(
441            requesting_user_name=requesting_user_name,
442            job_name=job_name,
443            job_k_octets=job_k_octets)
444       
445        # send the document
446        self.send_document(
447            job_id,
448            document,
449            document_name=document_name,
450            document_format=document_format,
451            document_natural_language=document_natural_language,
452            requesting_user_name=requesting_user_name,
453            compression=compression,
454            last_document=False)
455
456        return job_id
457
458    @sync
459    def verify_job(self, document_name=None, document_format=None,
460                  document_natural_language=None, requesting_user_name=None,
461                  compression=None, job_name=None, job_k_octets=None):
462
463        self.assert_running()
464
465        job_id = self._next_job_id
466        job = GutenbachJob(
467            job_id,
468            creator=requesting_user_name,
469            name=job_name)
470        job.spool(tempfile.TemporaryFile())
471        job.abort()
472        del job
473
474    @sync
475    def get_jobs(self, requesting_user_name=None, which_jobs=None,
476                 requested_attributes=None):
477       
478        self.assert_running()
479
480        # Filter by the which-jobs attribute
481        if which_jobs is None:
482            which_jobs = "not-completed"
483
484        if which_jobs == "completed":
485            jobs = [self.jobs[job_id] for job_id in self.finished_jobs]
486        elif which_jobs == "not-completed":
487            jobs = [self.jobs[job_id] for job_id in self.active_jobs]
488        else:
489            raise ipp.errors.ClientErrorAttributes(
490                which_jobs, ipp.WhichJobs(which_jobs))
491
492        # Filter by username
493        if requesting_user_name is None:
494            user_jobs = jobs
495        else:
496            user_jobs = [job for job in jobs if job.creator == requesting_user_name]
497
498        # Get the attributes of each job
499        job_attrs = [self.get_job_attributes(
500            job.id, requested_attributes=requested_attributes) for job in user_jobs]
501       
502        return job_attrs
503
504    @sync
505    def print_uri(self):
506        self.assert_running()
507
508    @sync
509    def create_job(self, requesting_user_name=None, job_name=None,
510                   job_k_octets=None):
511
512        self.assert_running()
513
514        job_id = self._next_job_id
515        self._next_job_id += 1
516       
517        job = GutenbachJob(
518            job_id,
519            creator=requesting_user_name,
520            name=job_name)
521
522        self.jobs[job_id] = job
523        self.pending_jobs.append(job_id)
524       
525        return job_id
526
527    @sync
528    def pause_printer(self):
529        """Pause the printer.
530
531        Does nothing if the printer is already paused.
532        """
533       
534        self.assert_running()
535        if not self.paused:
536            if self.current_job is not None and self.current_job.is_playing:
537                self.current_job.pause()
538            self.paused = True
539            logger.info("%s paused", str(self))
540
541    @sync
542    def resume_printer(self):
543        """Resume the printer.
544
545        Does nothing if the printer is not paused.
546        """
547       
548        self.assert_running()
549        if self.paused:
550            if self.current_job is not None:
551                self.current_job.resume()
552            self.paused = False
553            logger.info("%s unpaused", str(self))
554
555    @sync
556    def get_printer_attributes(self, requested_attributes=None):
557        self.assert_running()
558        if requested_attributes is None:
559            requested = self.printer_attributes
560        else:
561            requested = [a for a in self.printer_attributes \
562                         if a in requested_attributes]
563
564        _attributes = [attr.replace("-", "_") for attr in requested]
565        attributes = [getattr(self, attr) for attr in _attributes]
566        return attributes
567
568    @sync
569    def set_printer_attributes(self, job_id, attributes):
570        self.assert_running()
571        for attr in attributes:
572            try:
573                setattr(self, attr, attributes[attr])
574            except AttributeError:
575                raise ipp.errors.ClientErrorAttributes
576
577    @sync
578    def cancel_job(self, job_id, requesting_user_name=None):
579        self.assert_running()
580        job = self.get_job(job_id)
581        try:
582            job.cancel()
583        except InvalidJobStateException:
584            # XXX
585            raise
586
587    @sync
588    def send_document(self, job_id, document, document_name=None,
589                      document_format=None, document_natural_language=None,
590                      requesting_user_name=None, compression=None,
591                      last_document=None):
592
593        self.assert_running()
594        job = self.get_job(job_id)
595        job.spool(document)
596
597    @sync
598    def send_uri(self, job_id, document_uri, document_name=None,
599                 document_format=None, document_natural_language=None,
600                 requesting_user_name=None, compression=None,
601                 last_document=None):
602
603        self.assert_running()
604        job = self.get_job(job_id)
605        # XXX: need to validate URI
606        # XXX: need to deal with the URI stream?
607        #job.spool_uri(document_uri)
608
609    @sync
610    def get_job_attributes(self, job_id, requested_attributes=None):
611
612        self.assert_running()
613        if requested_attributes is None:
614            requested = self.job_attributes
615        else:
616            requested = [a for a in self.job_attributes \
617                         if a in requested_attributes]
618
619        _attributes = [attr.replace("-", "_") for attr in requested]
620        attributes = [getattr(self, attr)(job_id) for attr in _attributes]
621        return attributes
622
623    @sync
624    def set_job_attributes(self, job_id, attributes):
625
626        self.assert_running()
627        job = self.get_job(job_id)
628        for attr in attributes:
629            if attr in ("job-id", "job-k-octets", "job-state", "job-printer-uri"):
630                raise ipp.errors.ClientErrorAttributesNotSettable(attr)
631            elif attr == "job-name":
632                job.name = attributes[attr]
633            elif attr == "job-originating-user-name":
634                job.creator = attributes[attr] # XXX: do we want this?
635
636    @sync
637    def restart_job(self, job_id, requesting_user_name=None):
638
639        self.assert_running()
640        job = self.get_job(job_id)
641        try:
642            job.restart()
643        except InvalidJobStateException:
644            # XXX
645            raise ipp.errors.ClientErrorNotPossible
646
647        self.finished_jobs.remove(job_id)
648        self.pending_jobs.append(job_id)
649
650    @sync
651    def promote_job(self, job_id, requesting_user_name=None):
652        # According to RFC 3998, we need to put the job at the front
653        # of the queue (so that when the currently playing job
654        # completes, this one will go next
655       
656        self.assert_running()
657        job = self.get_job(job_id)
658        job.priority = 1 # XXX we need to actually do something
659                         # correct here
Note: See TracBrowser for help on using the repository browser.