source: server/lib/gutenbach/server/printer.py @ d42236e

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

Fix a few more bugs in printer.py and update test cases for printer.py

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