source: server/lib/gutenbach/server/printer.py @ 33528b4

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

Fix printer threading issues

  • Property mode set to 100644
File size: 18.6 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.current_job is not None:
139            return States.PROCESSING
140        elif len(self.pending_jobs) == 0:
141            return States.IDLE
142        else:
143            return States.STOPPED
144
145    @property
146    @sync
147    def active_jobs(self):
148        jobs = self.pending_jobs[:]
149        if self.current_job is not None:
150            jobs.insert(0, self.current_job.id)
151        return jobs
152
153    @property
154    def is_running(self):
155        running = self.ident is not None and self.isAlive() and self._running
156        return running
157
158    ######################################################################
159    ###                            Methods                             ###
160    ######################################################################
161
162    @sync
163    def start_job(self):
164        if not self.paused and self.current_job is None:
165            try:
166                job_id = heapq.heappop(self.pending_jobs)
167                self.current_job = self.get_job(job_id)
168                self.current_job.play()
169            except IndexError:
170                self.current_job = None
171            except InvalidJobStateException:
172                heapq.heappush(self.pending_jobs, self.current_job.id)
173                self.current_job = None
174                   
175    @sync
176    def complete_job(self):
177        if not self.paused and self.current_job is not None:
178            try:
179                if not self.current_job.is_done:
180                    self.current_job.stop()
181            finally:
182                self.finished_jobs.append(self.current_job.id)
183                self.current_job = None
184
185    @sync
186    def get_job(self, job_id):
187        if job_id not in self.jobs:
188            raise InvalidJobException(job_id)
189        return self.jobs[job_id]
190
191    ######################################################################
192    ###                        IPP Attributes                          ###
193    ######################################################################
194
195    @property
196    def printer_uri_supported(self):
197        return ipp.PrinterUriSupported(self.uri)
198    @printer_uri_supported.setter
199    def printer_uri_supported(self, val):
200        raise ipp.errors.AttributesNotSettable("printer-uri-supported")
201
202    @property
203    def uri_authentication_supported(self):
204        return ipp.UriAuthenticationSupported("none")
205    @uri_authentication_supported.setter
206    def uri_authentication_supported(self, val):
207        raise ipp.errors.AttributesNotSettable("uri-authentication-supported")
208
209    @property
210    def uri_security_supported(self):
211        return ipp.UriSecuritySupported("none")
212    @uri_security_supported.setter
213    def uri_security_supported(self, val):
214        raise ipp.errors.AttributesNotSettable("uri-security-supported")
215
216    @property
217    def printer_name(self):
218        return ipp.PrinterName(self.name)
219    @printer_name.setter
220    def printer_name(self, val):
221        raise ipp.errors.AttributesNotSettable("printer-name")
222
223    @property
224    def printer_state(self):
225        return ipp.PrinterState(self.state)
226    @printer_state.setter
227    def printer_state(self, val):
228        raise ipp.errors.AttributesNotSettable("printer-state")
229
230    @property
231    def printer_state_reasons(self):
232        return ipp.PrinterStateReasons("none")
233    @printer_state_reasons.setter
234    def printer_state_reasons(self, val):
235        raise ipp.errors.AttributesNotSettable("printer-state-reasons")
236
237    @property
238    def ipp_versions_supported(self):
239        return ipp.IppVersionsSupported(*self.config['ipp-versions'])
240    @ipp_versions_supported.setter
241    def ipp_versions_supported(self, val):
242        raise ipp.errors.AttributesNotSettable("ipp-versions-supported")
243
244    # XXX: We should query ourself for the supported operations
245    @property
246    def operations_supported(self):
247        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
248    @operations_supported.setter
249    def operations_supported(self, val):
250        raise ipp.errors.AttributesNotSettable("operations-supported")
251
252    @property
253    def charset_configured(self):
254        return ipp.CharsetConfigured("utf-8") # XXX
255    @charset_configured.setter
256    def charset_configured(self, val):
257        raise ipp.errors.AttributesNotSettable("charset-configured")
258       
259    @property
260    def charset_supported(self):
261        return ipp.CharsetSupported("utf-8") # XXX
262    @charset_supported.setter
263    def charset_supported(self, val):
264        raise ipp.errors.AttributesNotSettable("charset-supported")
265
266    @property
267    def natural_language_configured(self):
268        return ipp.NaturalLanguageConfigured("en-us")
269    @natural_language_configured.setter
270    def natural_language_configured(self, val):
271        raise ipp.errors.AttributesNotSettable("natural-language-configured")
272
273    @property
274    def generated_natural_language_supported(self):
275        return ipp.GeneratedNaturalLanguageSupported("en-us")
276    @generated_natural_language_supported.setter
277    def generated_natural_language_supported(self, val):
278        raise ipp.errors.AttributesNotSettable("generated-natural-language-supported")
279
280    @property
281    def document_format_default(self):
282        return ipp.DocumentFormatDefault("application/octet-stream")
283    @document_format_default.setter
284    def document_format_default(self, val):
285        raise ipp.errors.AttributesNotSettable("document-format-default")
286
287    @property
288    def document_format_supported(self):
289        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
290    @document_format_supported.setter
291    def document_format_supported(self, val):
292        raise ipp.errors.AttributesNotSettable("document-format-supported")
293
294    @property
295    def printer_is_accepting_jobs(self):
296        return ipp.PrinterIsAcceptingJobs(True)
297    @printer_is_accepting_jobs.setter
298    def printer_is_accepting_jobs(self, val):
299        raise ipp.errors.AttributesNotSettable("printer-is-accepting-jobs")
300
301    @property
302    def queued_job_count(self):
303        return ipp.QueuedJobCount(len(self.active_jobs))
304    @queued_job_count.setter
305    def queued_job_count(self, val):
306        raise ipp.errors.AttributesNotSettable("queued-job-count")
307
308    @property
309    def pdl_override_supported(self):
310        return ipp.PdlOverrideSupported("not-attempted")
311    @pdl_override_supported.setter
312    def pdl_override_supported(self, val):
313        raise ipp.errors.AttributesNotSettable("pdl-override-supported")
314
315    @property
316    def printer_up_time(self):
317        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
318    @printer_up_time.setter
319    def printer_up_time(self, val):
320        raise ipp.errors.AttributesNotSettable("printer-up-time")
321
322    @property
323    def compression_supported(self):
324        return ipp.CompressionSupported("none")
325    @compression_supported.setter
326    def compression_supported(self, val):
327        raise ipp.errors.AttributesNotSettable("compression-supported")
328
329    @property
330    def multiple_operation_time_out(self):
331        return ipp.MultipleOperationTimeOut(240)
332    @multiple_operation_time_out.setter
333    def multiple_operation_time_out(self, val):
334        raise ipp.errors.AttributesNotSettable("multiple-operation-time-out")
335
336    @property
337    def multiple_document_jobs_supported(self):
338        return ipp.MultipleDocumentJobsSupported(False)
339    @multiple_document_jobs_supported.setter
340    def multiple_document_jobs_supported(self, val):
341        raise ipp.errors.AttributesNotSettable("multiple-document-jobs-supported")
342
343    ######################################################################
344    ###                      Job IPP Attributes                        ###
345    ######################################################################
346
347    def job_id(self, job_id):
348        job = self.get_job(job_id)
349        return ipp.JobId(job.id)
350
351    def job_name(self, job_id):
352        job = self.get_job(job_id)
353        return ipp.JobName(job.name)
354
355    def job_originating_user_name(self, job_id):
356        job = self.get_job(job_id)
357        return ipp.JobOriginatingUserName(job.creator)
358
359    def job_k_octets(self, job_id):
360        job = self.get_job(job_id)
361        return ipp.JobKOctets(job.size)
362
363    def job_state(self, job_id):
364        job = self.get_job(job_id)
365        return ipp.JobState(job.state)
366
367    def job_printer_uri(self, job_id):
368        job = self.get_job(job_id)
369        return ipp.JobPrinterUri(self.uri)
370
371    ######################################################################
372    ###                        IPP Operations                          ###
373    ######################################################################
374
375    def print_job(self, document, document_name=None, document_format=None,
376                  document_natural_language=None, requesting_user_name=None,
377                  compression=None, job_name=None, job_k_octets=None):
378
379        # create the job
380        job_id = self.create_job(
381            requesting_user_name=requesting_user_name,
382            job_name=job_name,
383            job_k_octets=job_k_octets)
384       
385        # send the document
386        self.send_document(
387            job_id,
388            document,
389            document_name=document_name,
390            document_format=document_format,
391            document_natural_language=document_natural_language,
392            requesting_user_name=requesting_user_name,
393            compression=compression,
394            last_document=False)
395
396        return job_id
397
398    def verify_job(self, document_name=None, document_format=None,
399                  document_natural_language=None, requesting_user_name=None,
400                  compression=None, job_name=None, job_k_octets=None):
401
402        job_id = self._next_job_id
403        job = GutenbachJob(
404            job_id,
405            creator=requesting_user_name,
406            name=job_name)
407        job.spool(tempfile.TemporaryFile())
408        job.abort()
409        del job
410
411    def get_jobs(self, requesting_user_name=None, which_jobs=None,
412                 requested_attributes=None):
413       
414        # Filter by the which-jobs attribute
415        if which_jobs is None:
416            which_jobs = "not-completed"
417
418        if which_jobs == "completed":
419            jobs = [self.jobs[job_id] for job_id in self.finished_jobs]
420        elif which_jobs == "not-completed":
421            jobs = [self.jobs[job_id] for job_id in self.active_jobs]
422        else:
423            raise ipp.errors.ClientErrorAttributes(
424                which_jobs, ipp.WhichJobs(which_jobs))
425
426        # Filter by username
427        if requesting_user_name is None:
428            user_jobs = jobs
429        else:
430            user_jobs = [job for job in jobs if job.creator == requesting_user_name]
431
432        # Get the attributes of each job
433        job_attrs = [self.get_job_attributes(
434            job.id, requested_attributes=requested_attributes) for job in user_jobs]
435       
436        return job_attrs
437
438    def print_uri(self):
439        pass
440
441    def create_job(self, requesting_user_name=None, job_name=None,
442                   job_k_octets=None):
443
444        job_id = self._next_job_id
445        self._next_job_id += 1
446       
447        job = GutenbachJob(
448            job_id,
449            creator=requesting_user_name,
450            name=job_name)
451
452        self.jobs[job_id] = job
453        self.pending_jobs.append(job_id)
454       
455        return job_id
456
457    @sync
458    def pause_printer(self):
459        """Pause the printer.
460
461        Does nothing if the printer is already paused.
462        """
463       
464        if not self.paused:
465            if self.current_job is not None and self.current_job.is_playing:
466                self.current_job.pause()
467            self.paused = True
468            logger.info("%s paused", str(self))
469
470    @sync
471    def resume_printer(self):
472        """Resume the printer.
473
474        Does nothing if the printer is not paused.
475        """
476       
477        if self.paused:
478            if self.current_job is not None:
479                self.current_job.resume()
480            self.paused = False
481            logger.info("%s unpaused", str(self))
482
483    @sync
484    def get_printer_attributes(self, requested_attributes=None):
485        if requested_attributes is None:
486            requested = self.printer_attributes
487        else:
488            requested = [a for a in self.printer_attributes \
489                         if a in requested_attributes]
490
491        _attributes = [attr.replace("-", "_") for attr in requested]
492        attributes = [getattr(self, attr) for attr in _attributes]
493        return attributes
494
495    @sync
496    def set_printer_attributes(self, job_id, attributes):
497        for attr in attributes:
498            try:
499                setattr(self, attr, attributes[attr])
500            except AttributeError:
501                raise ipp.errors.ClientErrorAttributes
502
503    @sync
504    def cancel_job(self, job_id, requesting_user_name=None):
505        job = self.get_job(job_id)
506        try:
507            job.cancel()
508        except InvalidJobStateException:
509            # XXX
510            raise
511
512    @sync
513    def send_document(self, job_id, document, document_name=None,
514                      document_format=None, document_natural_language=None,
515                      requesting_user_name=None, compression=None,
516                      last_document=None):
517
518        job = self.get_job(job_id)
519        job.spool(document)
520
521    @sync
522    def send_uri(self, job_id, document_uri, document_name=None,
523                 document_format=None, document_natural_language=None,
524                 requesting_user_name=None, compression=None,
525                 last_document=None):
526        job = self.get_job(job_id)
527        # XXX: need to validate URI
528        # XXX: need to deal with the URI stream?
529        #job.spool_uri(document_uri)
530
531    @sync
532    def get_job_attributes(self, job_id, requested_attributes=None):
533        if requested_attributes is None:
534            requested = self.job_attributes
535        else:
536            requested = [a for a in self.job_attributes \
537                         if a in requested_attributes]
538
539        _attributes = [attr.replace("-", "_") for attr in requested]
540        attributes = [getattr(self, attr)(job_id) for attr in _attributes]
541        return attributes
542
543    @sync
544    def set_job_attributes(self, job_id, attributes):
545        job = self.get_job(job_id)
546        for attr in attributes:
547            if attr in ("job-id", "job-k-octets", "job-state", "job-printer-uri"):
548                raise ipp.errors.ClientErrorAttributesNotSettable(attr)
549            elif attr == "job-name":
550                job.name = attributes[attr]
551            elif attr == "job-originating-user-name":
552                job.creator = attributes[attr] # XXX: do we want this?
553
554    @sync
555    def restart_job(self, job_id, requesting_user_name=None):
556        job = self.get_job(job_id)
557        try:
558            job.restart()
559        except InvalidJobStateException:
560            # XXX
561            raise ipp.errors.ClientErrorNotPossible
562
563        self.finished_jobs.remove(job_id)
564        self.pending_jobs.append(job_id)
565
566    @sync
567    def promote_job(self, job_id, requesting_user_name=None):
568        # According to RFC 3998, we need to put the job at the front
569        # of the queue (so that when the currently playing job
570        # completes, this one will go next
571       
572        job = self.get_job(job_id)
573        job.priority = 1 # XXX we need to actually do something
574                         # correct here
Note: See TracBrowser for help on using the repository browser.