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
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
[57bc2dc]11import tempfile
[cf0d7e8]12from . import sync
[eee389a]13
[d04a689]14
15# initialize logger
16logger = logging.getLogger(__name__)
[776a659]17
[eee389a]18class GutenbachPrinter(threading.Thread):
[b2e077a]19
[1a63bf7]20    # for IPP
[33ea505]21    printer_attributes = [
[b2e077a]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",
[f6e2532]40        "compression-supported",
41        "multiple-operation-time-out",
42        "multiple-document-jobs-supported",
[1a63bf7]43    ]
[b2e077a]44
[33ea505]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
[f6e2532]54    operations = [
55        "print-job",
[33ea505]56        "validate-job",
[f6e2532]57        "get-jobs",
[33ea505]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"
[f6e2532]71    ]
72       
[609a9b0]73    def __init__(self, name, config, *args, **kwargs):
[eee389a]74
[d21198f]75        super(GutenbachPrinter, self).__init__(*args, **kwargs)
76       
77        self.name = name
[609a9b0]78        self.config = config
[d21198f]79        self.time_created = int(time.time())
[b2e077a]80
[d21198f]81        self.finished_jobs = []
82        self.pending_jobs = []
83        self.current_job = None
84        self.jobs = {}
[b2e077a]85
[d21198f]86        self.lock = threading.RLock()
[33528b4]87        self._running = False
[d21198f]88        self.paused = False
[776a659]89
[d21198f]90        # CUPS ignores jobs with id 0, so we have to start at 1
91        self._next_job_id = 1
[b01b6d1]92
[33528b4]93    @sync
[b01b6d1]94    def __repr__(self):
95        return str(self)
96
[33528b4]97    @sync
[b01b6d1]98    def __str__(self):
[33528b4]99        return "<Printer '%s'>" % self.name
[b01b6d1]100
[33ea505]101    def run(self):
[33528b4]102        self._running = True
103        while self._running:
104            try:
105                with self.lock:
106                    if self.current_job is None:
[33ea505]107                        self.start_job()
[345c476]108                    elif self.current_job.is_done:
[33ea505]109                        self.complete_job()
[33528b4]110            except:
111                logger.fatal(traceback.format_exc())
112                sys.exit(1)
[33ea505]113            time.sleep(0.1)
114
[33528b4]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
[b01b6d1]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
[cf0d7e8]136    @sync
[eee389a]137    def state(self):
[5e70cc2]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
[cf0d7e8]142        else:
[5e70cc2]143            state = States.STOPPED
144        return state
[eee389a]145
146    @property
[cf0d7e8]147    @sync
[eee389a]148    def active_jobs(self):
[cf0d7e8]149        jobs = self.pending_jobs[:]
150        if self.current_job is not None:
151            jobs.insert(0, self.current_job.id)
[eee389a]152        return jobs
[b01b6d1]153
[33528b4]154    @property
155    def is_running(self):
156        running = self.ident is not None and self.isAlive() and self._running
157        return running
158
[b01b6d1]159    ######################################################################
160    ###                            Methods                             ###
161    ######################################################################
162
[cf0d7e8]163    @sync
[5e70cc2]164    def assert_running(self):
165        if not self.is_running:
166            raise RuntimeError, "%s not started" % str(self)
167
168    @sync
[eee389a]169    def start_job(self):
[5e70cc2]170        self.assert_running()
[33528b4]171        if not self.paused and self.current_job is None:
[cf0d7e8]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
[eee389a]181                   
[cf0d7e8]182    @sync
[eee389a]183    def complete_job(self):
[5e70cc2]184        self.assert_running()
[33528b4]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
[1a63bf7]192
[cf0d7e8]193    @sync
[eee389a]194    def get_job(self, job_id):
[5e70cc2]195        self.assert_running()
[cf0d7e8]196        if job_id not in self.jobs:
197            raise InvalidJobException(job_id)
198        return self.jobs[job_id]
[b01b6d1]199
200    ######################################################################
201    ###                        IPP Attributes                          ###
202    ######################################################################
[1a63bf7]203
[b2e077a]204    @property
205    def printer_uri_supported(self):
[5e70cc2]206        self.assert_running()
[793432f]207        return ipp.PrinterUriSupported(self.uri)
[9da7428]208    @printer_uri_supported.setter
209    def printer_uri_supported(self, val):
[5e70cc2]210        self.assert_running()
[9da7428]211        raise ipp.errors.AttributesNotSettable("printer-uri-supported")
[1a63bf7]212
[b2e077a]213    @property
214    def uri_authentication_supported(self):
[5e70cc2]215        self.assert_running()
[793432f]216        return ipp.UriAuthenticationSupported("none")
[9da7428]217    @uri_authentication_supported.setter
218    def uri_authentication_supported(self, val):
[5e70cc2]219        self.assert_running()
[9da7428]220        raise ipp.errors.AttributesNotSettable("uri-authentication-supported")
[b2e077a]221
222    @property
223    def uri_security_supported(self):
[5e70cc2]224        self.assert_running()
[793432f]225        return ipp.UriSecuritySupported("none")
[9da7428]226    @uri_security_supported.setter
227    def uri_security_supported(self, val):
[5e70cc2]228        self.assert_running()
[9da7428]229        raise ipp.errors.AttributesNotSettable("uri-security-supported")
[b2e077a]230
231    @property
232    def printer_name(self):
[5e70cc2]233        self.assert_running()
[793432f]234        return ipp.PrinterName(self.name)
[9da7428]235    @printer_name.setter
236    def printer_name(self, val):
[5e70cc2]237        self.assert_running()
[9da7428]238        raise ipp.errors.AttributesNotSettable("printer-name")
[1a63bf7]239
[b2e077a]240    @property
241    def printer_state(self):
[5e70cc2]242        self.assert_running()
[b01b6d1]243        return ipp.PrinterState(self.state)
[9da7428]244    @printer_state.setter
245    def printer_state(self, val):
[5e70cc2]246        self.assert_running()
[9da7428]247        raise ipp.errors.AttributesNotSettable("printer-state")
[1a63bf7]248
[b2e077a]249    @property
250    def printer_state_reasons(self):
[5e70cc2]251        self.assert_running()
[793432f]252        return ipp.PrinterStateReasons("none")
[9da7428]253    @printer_state_reasons.setter
254    def printer_state_reasons(self, val):
[5e70cc2]255        self.assert_running()
[9da7428]256        raise ipp.errors.AttributesNotSettable("printer-state-reasons")
[b2e077a]257
258    @property
259    def ipp_versions_supported(self):
[5e70cc2]260        self.assert_running()
[609a9b0]261        return ipp.IppVersionsSupported(*self.config['ipp-versions'])
[9da7428]262    @ipp_versions_supported.setter
263    def ipp_versions_supported(self, val):
[5e70cc2]264        self.assert_running()
[9da7428]265        raise ipp.errors.AttributesNotSettable("ipp-versions-supported")
[1a63bf7]266
[f6e2532]267    # XXX: We should query ourself for the supported operations
[b2e077a]268    @property
269    def operations_supported(self):
[5e70cc2]270        self.assert_running()
[793432f]271        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
[9da7428]272    @operations_supported.setter
273    def operations_supported(self, val):
[5e70cc2]274        self.assert_running()
[9da7428]275        raise ipp.errors.AttributesNotSettable("operations-supported")
[b2e077a]276
277    @property
278    def charset_configured(self):
[5e70cc2]279        self.assert_running()
[9da7428]280        return ipp.CharsetConfigured("utf-8") # XXX
281    @charset_configured.setter
282    def charset_configured(self, val):
[5e70cc2]283        self.assert_running()
[9da7428]284        raise ipp.errors.AttributesNotSettable("charset-configured")
285       
[b2e077a]286    @property
287    def charset_supported(self):
[5e70cc2]288        self.assert_running()
[9da7428]289        return ipp.CharsetSupported("utf-8") # XXX
290    @charset_supported.setter
291    def charset_supported(self, val):
[5e70cc2]292        self.assert_running()
[9da7428]293        raise ipp.errors.AttributesNotSettable("charset-supported")
[b2e077a]294
295    @property
296    def natural_language_configured(self):
[5e70cc2]297        self.assert_running()
[793432f]298        return ipp.NaturalLanguageConfigured("en-us")
[9da7428]299    @natural_language_configured.setter
300    def natural_language_configured(self, val):
[5e70cc2]301        self.assert_running()
[9da7428]302        raise ipp.errors.AttributesNotSettable("natural-language-configured")
[b2e077a]303
304    @property
305    def generated_natural_language_supported(self):
[5e70cc2]306        self.assert_running()
[793432f]307        return ipp.GeneratedNaturalLanguageSupported("en-us")
[9da7428]308    @generated_natural_language_supported.setter
309    def generated_natural_language_supported(self, val):
[5e70cc2]310        self.assert_running()
[9da7428]311        raise ipp.errors.AttributesNotSettable("generated-natural-language-supported")
[b2e077a]312
313    @property
314    def document_format_default(self):
[5e70cc2]315        self.assert_running()
[793432f]316        return ipp.DocumentFormatDefault("application/octet-stream")
[9da7428]317    @document_format_default.setter
318    def document_format_default(self, val):
[5e70cc2]319        self.assert_running()
[9da7428]320        raise ipp.errors.AttributesNotSettable("document-format-default")
[b2e077a]321
322    @property
323    def document_format_supported(self):
[5e70cc2]324        self.assert_running()
[793432f]325        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
[9da7428]326    @document_format_supported.setter
327    def document_format_supported(self, val):
[5e70cc2]328        self.assert_running()
[9da7428]329        raise ipp.errors.AttributesNotSettable("document-format-supported")
[b2e077a]330
331    @property
332    def printer_is_accepting_jobs(self):
[5e70cc2]333        self.assert_running()
[793432f]334        return ipp.PrinterIsAcceptingJobs(True)
[9da7428]335    @printer_is_accepting_jobs.setter
336    def printer_is_accepting_jobs(self, val):
[5e70cc2]337        self.assert_running()
[9da7428]338        raise ipp.errors.AttributesNotSettable("printer-is-accepting-jobs")
[b2e077a]339
340    @property
341    def queued_job_count(self):
[5e70cc2]342        self.assert_running()
[793432f]343        return ipp.QueuedJobCount(len(self.active_jobs))
[9da7428]344    @queued_job_count.setter
345    def queued_job_count(self, val):
[5e70cc2]346        self.assert_running()
[9da7428]347        raise ipp.errors.AttributesNotSettable("queued-job-count")
[b2e077a]348
349    @property
350    def pdl_override_supported(self):
[5e70cc2]351        self.assert_running()
[793432f]352        return ipp.PdlOverrideSupported("not-attempted")
[9da7428]353    @pdl_override_supported.setter
354    def pdl_override_supported(self, val):
[5e70cc2]355        self.assert_running()
[9da7428]356        raise ipp.errors.AttributesNotSettable("pdl-override-supported")
[b2e077a]357
358    @property
359    def printer_up_time(self):
[5e70cc2]360        self.assert_running()
[793432f]361        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
[9da7428]362    @printer_up_time.setter
363    def printer_up_time(self, val):
[5e70cc2]364        self.assert_running()
[9da7428]365        raise ipp.errors.AttributesNotSettable("printer-up-time")
[b2e077a]366
367    @property
368    def compression_supported(self):
[5e70cc2]369        self.assert_running()
[793432f]370        return ipp.CompressionSupported("none")
[9da7428]371    @compression_supported.setter
372    def compression_supported(self, val):
[5e70cc2]373        self.assert_running()
[9da7428]374        raise ipp.errors.AttributesNotSettable("compression-supported")
[b2e077a]375
[f6e2532]376    @property
377    def multiple_operation_time_out(self):
[5e70cc2]378        self.assert_running()
[793432f]379        return ipp.MultipleOperationTimeOut(240)
[9da7428]380    @multiple_operation_time_out.setter
381    def multiple_operation_time_out(self, val):
[5e70cc2]382        self.assert_running()
[9da7428]383        raise ipp.errors.AttributesNotSettable("multiple-operation-time-out")
[f6e2532]384
385    @property
386    def multiple_document_jobs_supported(self):
[5e70cc2]387        self.assert_running()
[793432f]388        return ipp.MultipleDocumentJobsSupported(False)
[9da7428]389    @multiple_document_jobs_supported.setter
390    def multiple_document_jobs_supported(self, val):
[5e70cc2]391        self.assert_running()
[9da7428]392        raise ipp.errors.AttributesNotSettable("multiple-document-jobs-supported")
[f6e2532]393
[33ea505]394    ######################################################################
395    ###                      Job IPP Attributes                        ###
396    ######################################################################
397
398    def job_id(self, job_id):
[5e70cc2]399        self.assert_running()
[33ea505]400        job = self.get_job(job_id)
401        return ipp.JobId(job.id)
402
403    def job_name(self, job_id):
[5e70cc2]404        self.assert_running()
[33ea505]405        job = self.get_job(job_id)
406        return ipp.JobName(job.name)
407
408    def job_originating_user_name(self, job_id):
[5e70cc2]409        self.assert_running()
[33ea505]410        job = self.get_job(job_id)
411        return ipp.JobOriginatingUserName(job.creator)
412
413    def job_k_octets(self, job_id):
[5e70cc2]414        self.assert_running()
[33ea505]415        job = self.get_job(job_id)
416        return ipp.JobKOctets(job.size)
417
418    def job_state(self, job_id):
[5e70cc2]419        self.assert_running()
[33ea505]420        job = self.get_job(job_id)
421        return ipp.JobState(job.state)
422
423    def job_printer_uri(self, job_id):
[5e70cc2]424        self.assert_running()
[33ea505]425        job = self.get_job(job_id)
426        return ipp.JobPrinterUri(self.uri)
[ee8e6d0]427
[b01b6d1]428    ######################################################################
429    ###                        IPP Operations                          ###
430    ######################################################################
431
[5e70cc2]432    @sync
[57bc2dc]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):
[b01b6d1]436
[5e70cc2]437        self.assert_running()
438
[57bc2dc]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
[5e70cc2]458    @sync
[57bc2dc]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
[5e70cc2]463        self.assert_running()
464
[57bc2dc]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
[b01b6d1]473
[5e70cc2]474    @sync
[33ea505]475    def get_jobs(self, requesting_user_name=None, which_jobs=None,
476                 requested_attributes=None):
477       
[5e70cc2]478        self.assert_running()
479
[b01b6d1]480        # Filter by the which-jobs attribute
481        if which_jobs is None:
[34a4e5d]482            which_jobs = "not-completed"
483
484        if which_jobs == "completed":
[b01b6d1]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]
[ee8e6d0]488        else:
[b01b6d1]489            raise ipp.errors.ClientErrorAttributes(
490                which_jobs, ipp.WhichJobs(which_jobs))
[b2e077a]491
[b01b6d1]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]
[33ea505]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]
[ee8e6d0]501       
[33ea505]502        return job_attrs
[ee8e6d0]503
[5e70cc2]504    @sync
[b01b6d1]505    def print_uri(self):
[5e70cc2]506        self.assert_running()
[b01b6d1]507
[5e70cc2]508    @sync
[57bc2dc]509    def create_job(self, requesting_user_name=None, job_name=None,
510                   job_k_octets=None):
511
[5e70cc2]512        self.assert_running()
513
[eee389a]514        job_id = self._next_job_id
515        self._next_job_id += 1
[ee8e6d0]516       
[33ea505]517        job = GutenbachJob(
518            job_id,
519            creator=requesting_user_name,
520            name=job_name)
[57bc2dc]521
[ee8e6d0]522        self.jobs[job_id] = job
[eee389a]523        self.pending_jobs.append(job_id)
[b01b6d1]524       
[33ea505]525        return job_id
[776a659]526
[fa3e2c6]527    @sync
[b01b6d1]528    def pause_printer(self):
[fa3e2c6]529        """Pause the printer.
530
531        Does nothing if the printer is already paused.
532        """
[33528b4]533       
[5e70cc2]534        self.assert_running()
[33528b4]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))
[fa3e2c6]540
541    @sync
[b01b6d1]542    def resume_printer(self):
[fa3e2c6]543        """Resume the printer.
544
545        Does nothing if the printer is not paused.
546        """
[33528b4]547       
[5e70cc2]548        self.assert_running()
[33528b4]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))
[776a659]554
[57bc2dc]555    @sync
[b01b6d1]556    def get_printer_attributes(self, requested_attributes=None):
[5e70cc2]557        self.assert_running()
[b01b6d1]558        if requested_attributes is None:
[33ea505]559            requested = self.printer_attributes
[e58af05]560        else:
[33ea505]561            requested = [a for a in self.printer_attributes \
562                         if a in requested_attributes]
[b2e077a]563
[b01b6d1]564        _attributes = [attr.replace("-", "_") for attr in requested]
565        attributes = [getattr(self, attr) for attr in _attributes]
566        return attributes
[776a659]567
[57bc2dc]568    @sync
[9da7428]569    def set_printer_attributes(self, job_id, attributes):
[5e70cc2]570        self.assert_running()
[9da7428]571        for attr in attributes:
572            try:
573                setattr(self, attr, attributes[attr])
574            except AttributeError:
575                raise ipp.errors.ClientErrorAttributes
[33ea505]576
[57bc2dc]577    @sync
[33ea505]578    def cancel_job(self, job_id, requesting_user_name=None):
[5e70cc2]579        self.assert_running()
[33ea505]580        job = self.get_job(job_id)
581        try:
582            job.cancel()
583        except InvalidJobStateException:
584            # XXX
585            raise
586
[57bc2dc]587    @sync
[33ea505]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
[5e70cc2]593        self.assert_running()
[33ea505]594        job = self.get_job(job_id)
[345c476]595        job.spool(document)
[33ea505]596
[57bc2dc]597    @sync
[c1dc25f]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):
[5e70cc2]602
603        self.assert_running()
[c1dc25f]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)
[33ea505]608
[57bc2dc]609    @sync
[33ea505]610    def get_job_attributes(self, job_id, requested_attributes=None):
[5e70cc2]611
612        self.assert_running()
[33ea505]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
[57bc2dc]623    @sync
[9da7428]624    def set_job_attributes(self, job_id, attributes):
[5e70cc2]625
626        self.assert_running()
[9da7428]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?
[57bc2dc]635
636    @sync
[c1cebbc]637    def restart_job(self, job_id, requesting_user_name=None):
[5e70cc2]638
639        self.assert_running()
[c1cebbc]640        job = self.get_job(job_id)
641        try:
642            job.restart()
643        except InvalidJobStateException:
644            # XXX
645            raise ipp.errors.ClientErrorNotPossible
[33ea505]646
[57bc2dc]647        self.finished_jobs.remove(job_id)
648        self.pending_jobs.append(job_id)
[33ea505]649
[57bc2dc]650    @sync
[c1cebbc]651    def promote_job(self, job_id, requesting_user_name=None):
[c500bc2]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       
[5e70cc2]656        self.assert_running()
[c500bc2]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.