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

no-cups
Last change on this file since 9225351 was 9225351, checked in by Steven Allen <steven@…>, 12 years ago

Merge branch 'no-cups' of github.com:jhamrick/gutenbach into no-cups

  • Property mode set to 100644
File size: 16.9 KB
RevLine 
[33ea505]1from .errors import InvalidJobException, InvalidPrinterStateException, InvalidJobStateException
2from .job import GutenbachJob
[b01b6d1]3from gutenbach.ipp import PrinterStates as States
[b2e077a]4import gutenbach.ipp as ipp
5import logging
6import time
[eee389a]7import threading
8import heapq
9import traceback
10import sys
[cf0d7e8]11from . import sync
[eee389a]12
[d04a689]13
14# initialize logger
15logger = logging.getLogger(__name__)
[776a659]16
[eee389a]17class GutenbachPrinter(threading.Thread):
[b2e077a]18
[1a63bf7]19    # for IPP
[33ea505]20    printer_attributes = [
[b2e077a]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",
[f6e2532]39        "compression-supported",
40        "multiple-operation-time-out",
41        "multiple-document-jobs-supported",
[1a63bf7]42    ]
[b2e077a]43
[33ea505]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
[f6e2532]53    operations = [
54        "print-job",
[33ea505]55        "validate-job",
[f6e2532]56        "get-jobs",
[33ea505]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"
[f6e2532]70    ]
71       
[609a9b0]72    def __init__(self, name, config, *args, **kwargs):
[eee389a]73
[d21198f]74        super(GutenbachPrinter, self).__init__(*args, **kwargs)
75       
76        self.name = name
[609a9b0]77        self.config = config
[d21198f]78        self.time_created = int(time.time())
[b2e077a]79
[d21198f]80        self.finished_jobs = []
81        self.pending_jobs = []
82        self.current_job = None
83        self.jobs = {}
[b2e077a]84
[d21198f]85        self.lock = threading.RLock()
86        self.running = False
87        self.paused = False
[776a659]88
[d21198f]89        # CUPS ignores jobs with id 0, so we have to start at 1
90        self._next_job_id = 1
[b01b6d1]91
92    def __repr__(self):
93        return str(self)
94
95    def __str__(self):
96        return "<Printer '%s'>" % self.name
97
[33ea505]98    def run(self):
99        self.running = True
100        while self.running:
101            with self.lock:
102                try:
[fa3e2c6]103                    if not self.paused and self.current_job is None:
[33ea505]104                        self.start_job()
[345c476]105                    elif self.current_job.is_done:
[33ea505]106                        self.complete_job()
107                except:
108                    logger.fatal(traceback.format_exc())
109                    sys.exit(1)
110            time.sleep(0.1)
111
[b01b6d1]112    ######################################################################
113    ###                          Properties                            ###
114    ######################################################################
115
116    @property
117    def uris(self):
118        uris = ["ipp://localhost:8000/printers/" + self.name,
119                "ipp://localhost/printers/" + self.name]
120        return uris
121   
122    @property
123    def uri(self):
124        return self.uris[0]
125
126    @property
[cf0d7e8]127    @sync
[eee389a]128    def state(self):
[cf0d7e8]129        if self.current_job is not None:
130            return States.PROCESSING
131        elif len(self.pending_jobs) == 0:
132            return States.IDLE
133        else:
134            return States.STOPPED
[eee389a]135
136    @property
[cf0d7e8]137    @sync
[eee389a]138    def active_jobs(self):
[cf0d7e8]139        jobs = self.pending_jobs[:]
140        if self.current_job is not None:
141            jobs.insert(0, self.current_job.id)
[eee389a]142        return jobs
[b01b6d1]143
144    ######################################################################
145    ###                            Methods                             ###
146    ######################################################################
147
[cf0d7e8]148    @sync
[eee389a]149    def start_job(self):
[cf0d7e8]150        if self.current_job is None:
151            try:
152                job_id = heapq.heappop(self.pending_jobs)
153                self.current_job = self.get_job(job_id)
154                self.current_job.play()
155            except IndexError:
156                self.current_job = None
157            except InvalidJobStateException:
158                heapq.heappush(self.pending_jobs, self.current_job.id)
159                self.current_job = None
[eee389a]160                   
[cf0d7e8]161    @sync
[eee389a]162    def complete_job(self):
[cf0d7e8]163        if self.current_job is None:
164            return
[eee389a]165
[cf0d7e8]166        try:
167            if not self.current_job.is_done:
168                self.current_job.stop()
169        finally:
170            self.finished_jobs.append(self.current_job.id)
171            self.current_job = None
[1a63bf7]172
[cf0d7e8]173    @sync
[eee389a]174    def get_job(self, job_id):
[cf0d7e8]175        if job_id not in self.jobs:
176            raise InvalidJobException(job_id)
177        return self.jobs[job_id]
[b01b6d1]178
179    ######################################################################
180    ###                        IPP Attributes                          ###
181    ######################################################################
[1a63bf7]182
[b2e077a]183    @property
184    def printer_uri_supported(self):
[793432f]185        return ipp.PrinterUriSupported(self.uri)
[9da7428]186    @printer_uri_supported.setter
187    def printer_uri_supported(self, val):
188        raise ipp.errors.AttributesNotSettable("printer-uri-supported")
[1a63bf7]189
[b2e077a]190    @property
191    def uri_authentication_supported(self):
[793432f]192        return ipp.UriAuthenticationSupported("none")
[9da7428]193    @uri_authentication_supported.setter
194    def uri_authentication_supported(self, val):
195        raise ipp.errors.AttributesNotSettable("uri-authentication-supported")
[b2e077a]196
197    @property
198    def uri_security_supported(self):
[793432f]199        return ipp.UriSecuritySupported("none")
[9da7428]200    @uri_security_supported.setter
201    def uri_security_supported(self, val):
202        raise ipp.errors.AttributesNotSettable("uri-security-supported")
[b2e077a]203
204    @property
205    def printer_name(self):
[793432f]206        return ipp.PrinterName(self.name)
[9da7428]207    @printer_name.setter
208    def printer_name(self, val):
209        raise ipp.errors.AttributesNotSettable("printer-name")
[1a63bf7]210
[b2e077a]211    @property
212    def printer_state(self):
[b01b6d1]213        return ipp.PrinterState(self.state)
[9da7428]214    @printer_state.setter
215    def printer_state(self, val):
216        raise ipp.errors.AttributesNotSettable("printer-state")
[1a63bf7]217
[b2e077a]218    @property
219    def printer_state_reasons(self):
[793432f]220        return ipp.PrinterStateReasons("none")
[9da7428]221    @printer_state_reasons.setter
222    def printer_state_reasons(self, val):
223        raise ipp.errors.AttributesNotSettable("printer-state-reasons")
[b2e077a]224
225    @property
226    def ipp_versions_supported(self):
[609a9b0]227        return ipp.IppVersionsSupported(*self.config['ipp-versions'])
[9da7428]228    @ipp_versions_supported.setter
229    def ipp_versions_supported(self, val):
230        raise ipp.errors.AttributesNotSettable("ipp-versions-supported")
[1a63bf7]231
[f6e2532]232    # XXX: We should query ourself for the supported operations
[b2e077a]233    @property
234    def operations_supported(self):
[793432f]235        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
[9da7428]236    @operations_supported.setter
237    def operations_supported(self, val):
238        raise ipp.errors.AttributesNotSettable("operations-supported")
[b2e077a]239
240    @property
241    def charset_configured(self):
[9da7428]242        return ipp.CharsetConfigured("utf-8") # XXX
243    @charset_configured.setter
244    def charset_configured(self, val):
245        raise ipp.errors.AttributesNotSettable("charset-configured")
246       
[b2e077a]247    @property
248    def charset_supported(self):
[9da7428]249        return ipp.CharsetSupported("utf-8") # XXX
250    @charset_supported.setter
251    def charset_supported(self, val):
252        raise ipp.errors.AttributesNotSettable("charset-supported")
[b2e077a]253
254    @property
255    def natural_language_configured(self):
[793432f]256        return ipp.NaturalLanguageConfigured("en-us")
[9da7428]257    @natural_language_configured.setter
258    def natural_language_configured(self, val):
259        raise ipp.errors.AttributesNotSettable("natural-language-configured")
[b2e077a]260
261    @property
262    def generated_natural_language_supported(self):
[793432f]263        return ipp.GeneratedNaturalLanguageSupported("en-us")
[9da7428]264    @generated_natural_language_supported.setter
265    def generated_natural_language_supported(self, val):
266        raise ipp.errors.AttributesNotSettable("generated-natural-language-supported")
[b2e077a]267
268    @property
269    def document_format_default(self):
[793432f]270        return ipp.DocumentFormatDefault("application/octet-stream")
[9da7428]271    @document_format_default.setter
272    def document_format_default(self, val):
273        raise ipp.errors.AttributesNotSettable("document-format-default")
[b2e077a]274
275    @property
276    def document_format_supported(self):
[793432f]277        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
[9da7428]278    @document_format_supported.setter
279    def document_format_supported(self, val):
280        raise ipp.errors.AttributesNotSettable("document-format-supported")
[b2e077a]281
282    @property
283    def printer_is_accepting_jobs(self):
[793432f]284        return ipp.PrinterIsAcceptingJobs(True)
[9da7428]285    @printer_is_accepting_jobs.setter
286    def printer_is_accepting_jobs(self, val):
287        raise ipp.errors.AttributesNotSettable("printer-is-accepting-jobs")
[b2e077a]288
289    @property
290    def queued_job_count(self):
[793432f]291        return ipp.QueuedJobCount(len(self.active_jobs))
[9da7428]292    @queued_job_count.setter
293    def queued_job_count(self, val):
294        raise ipp.errors.AttributesNotSettable("queued-job-count")
[b2e077a]295
296    @property
297    def pdl_override_supported(self):
[793432f]298        return ipp.PdlOverrideSupported("not-attempted")
[9da7428]299    @pdl_override_supported.setter
300    def pdl_override_supported(self, val):
301        raise ipp.errors.AttributesNotSettable("pdl-override-supported")
[b2e077a]302
303    @property
304    def printer_up_time(self):
[793432f]305        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
[9da7428]306    @printer_up_time.setter
307    def printer_up_time(self, val):
308        raise ipp.errors.AttributesNotSettable("printer-up-time")
[b2e077a]309
310    @property
311    def compression_supported(self):
[793432f]312        return ipp.CompressionSupported("none")
[9da7428]313    @compression_supported.setter
314    def compression_supported(self, val):
315        raise ipp.errors.AttributesNotSettable("compression-supported")
[b2e077a]316
[f6e2532]317    @property
318    def multiple_operation_time_out(self):
[793432f]319        return ipp.MultipleOperationTimeOut(240)
[9da7428]320    @multiple_operation_time_out.setter
321    def multiple_operation_time_out(self, val):
322        raise ipp.errors.AttributesNotSettable("multiple-operation-time-out")
[f6e2532]323
324    @property
325    def multiple_document_jobs_supported(self):
[793432f]326        return ipp.MultipleDocumentJobsSupported(False)
[9da7428]327    @multiple_document_jobs_supported.setter
328    def multiple_document_jobs_supported(self, val):
329        raise ipp.errors.AttributesNotSettable("multiple-document-jobs-supported")
[f6e2532]330
[33ea505]331    ######################################################################
332    ###                      Job IPP Attributes                        ###
333    ######################################################################
334
335    def job_id(self, job_id):
336        job = self.get_job(job_id)
337        return ipp.JobId(job.id)
338
339    def job_name(self, job_id):
340        job = self.get_job(job_id)
341        return ipp.JobName(job.name)
342
343    def job_originating_user_name(self, job_id):
344        job = self.get_job(job_id)
345        return ipp.JobOriginatingUserName(job.creator)
346
347    def job_k_octets(self, job_id):
348        job = self.get_job(job_id)
349        return ipp.JobKOctets(job.size)
350
351    def job_state(self, job_id):
352        job = self.get_job(job_id)
353        return ipp.JobState(job.state)
354
355    def job_printer_uri(self, job_id):
356        job = self.get_job(job_id)
357        return ipp.JobPrinterUri(self.uri)
[ee8e6d0]358
[b01b6d1]359    ######################################################################
360    ###                        IPP Operations                          ###
361    ######################################################################
362
363    def print_job(self):
364        pass
365
366    def validate_job(self):
367        pass
368
[33ea505]369    def get_jobs(self, requesting_user_name=None, which_jobs=None,
370                 requested_attributes=None):
371       
[b01b6d1]372        # Filter by the which-jobs attribute
373        if which_jobs is None:
[34a4e5d]374            which_jobs = "not-completed"
375
376        if which_jobs == "completed":
[b01b6d1]377            jobs = [self.jobs[job_id] for job_id in self.finished_jobs]
378        elif which_jobs == "not-completed":
379            jobs = [self.jobs[job_id] for job_id in self.active_jobs]
[ee8e6d0]380        else:
[b01b6d1]381            raise ipp.errors.ClientErrorAttributes(
382                which_jobs, ipp.WhichJobs(which_jobs))
[b2e077a]383
[b01b6d1]384        # Filter by username
385        if requesting_user_name is None:
386            user_jobs = jobs
387        else:
388            user_jobs = [job for job in jobs if job.creator == requesting_user_name]
[33ea505]389
390        # Get the attributes of each job
391        job_attrs = [self.get_job_attributes(
392            job.id, requested_attributes=requested_attributes) for job in user_jobs]
[ee8e6d0]393       
[33ea505]394        return job_attrs
[ee8e6d0]395
[b01b6d1]396    def print_uri(self):
397        pass
398
[190bfb4]399    def create_job(self, requesting_user_name=None, job_name=None, job_k_octets=None):
[eee389a]400        job_id = self._next_job_id
401        self._next_job_id += 1
[ee8e6d0]402       
[33ea505]403        job = GutenbachJob(
404            job_id,
405            creator=requesting_user_name,
406            name=job_name)
[b01b6d1]407       
[ee8e6d0]408        self.jobs[job_id] = job
[eee389a]409        self.pending_jobs.append(job_id)
[b01b6d1]410       
[33ea505]411        return job_id
[776a659]412
[fa3e2c6]413    @sync
[b01b6d1]414    def pause_printer(self):
[fa3e2c6]415        """Pause the printer.
416
417        Does nothing if the printer is already paused.
418        """
419        if self.paused:
420            return
421
422        if self.current_job is not None and self.current_job.is_playing:
423            self.current_job.pause()
424
425        self.paused = True
426
427
[776a659]428
[fa3e2c6]429    @sync
[b01b6d1]430    def resume_printer(self):
[fa3e2c6]431        """Resume the printer.
432
433        Does nothing if the printer is not paused.
434        """
435        if not self.paused:
436            return
437
438        if self.current_job is not None:
439            self.current_job.resume()
440
441        self.paused = False
[776a659]442
[b01b6d1]443    def get_printer_attributes(self, requested_attributes=None):
444        if requested_attributes is None:
[33ea505]445            requested = self.printer_attributes
[e58af05]446        else:
[33ea505]447            requested = [a for a in self.printer_attributes \
448                         if a in requested_attributes]
[b2e077a]449
[b01b6d1]450        _attributes = [attr.replace("-", "_") for attr in requested]
451        attributes = [getattr(self, attr) for attr in _attributes]
452        return attributes
[776a659]453
[9da7428]454    def set_printer_attributes(self, job_id, attributes):
455        for attr in attributes:
456            try:
457                setattr(self, attr, attributes[attr])
458            except AttributeError:
459                raise ipp.errors.ClientErrorAttributes
[33ea505]460
461    def cancel_job(self, job_id, requesting_user_name=None):
462        job = self.get_job(job_id)
463        try:
464            job.cancel()
465        except InvalidJobStateException:
466            # XXX
467            raise
468
469    def send_document(self, job_id, document, document_name=None,
470                      document_format=None, document_natural_language=None,
471                      requesting_user_name=None, compression=None,
472                      last_document=None):
473
474        job = self.get_job(job_id)
[345c476]475        job.spool(document)
[33ea505]476
[c1dc25f]477    def send_uri(self, job_id, document_uri, document_name=None,
478                 document_format=None, document_natural_language=None,
479                 requesting_user_name=None, compression=None,
480                 last_document=None):
481        job = self.get_job(job_id)
482        # XXX: need to validate URI
483        # XXX: need to deal with the URI stream?
484        #job.spool_uri(document_uri)
[33ea505]485
486    def get_job_attributes(self, job_id, requested_attributes=None):
487        if requested_attributes is None:
488            requested = self.job_attributes
489        else:
490            requested = [a for a in self.job_attributes \
491                         if a in requested_attributes]
492
493        _attributes = [attr.replace("-", "_") for attr in requested]
494        attributes = [getattr(self, attr)(job_id) for attr in _attributes]
495        return attributes
496
[9da7428]497    def set_job_attributes(self, job_id, attributes):
498        job = self.get_job(job_id)
499        for attr in attributes:
500            if attr in ("job-id", "job-k-octets", "job-state", "job-printer-uri"):
501                raise ipp.errors.ClientErrorAttributesNotSettable(attr)
502            elif attr == "job-name":
503                job.name = attributes[attr]
504            elif attr == "job-originating-user-name":
505                job.creator = attributes[attr] # XXX: do we want this?
506               
[c1cebbc]507    def restart_job(self, job_id, requesting_user_name=None):
508        job = self.get_job(job_id)
509        try:
510            job.restart()
511        except InvalidJobStateException:
512            # XXX
513            raise ipp.errors.ClientErrorNotPossible
[33ea505]514
[c1cebbc]515        with self.lock:
516            self.finished_jobs.remove(job_id)
517            self.pending_jobs.append(job_id)
[33ea505]518
[c1cebbc]519    def promote_job(self, job_id, requesting_user_name=None):
[c500bc2]520        # According to RFC 3998, we need to put the job at the front
521        # of the queue (so that when the currently playing job
522        # completes, this one will go next
523       
524        job = self.get_job(job_id)
525        job.priority = 1 # XXX we need to actually do something
526                         # correct here
Note: See TracBrowser for help on using the repository browser.