source: server/lib/gutenbach/server/job.py @ 739d696

Last change on this file since 739d696 was 3c0760f, checked in by Steven Allen <steven@…>, 12 years ago

Block instead of looping and sleeping.

  • Property mode set to 100644
File size: 17.8 KB
RevLine 
[951ab1b]1from . import errors
[eee389a]2from .player import Player
[b01b6d1]3from gutenbach.ipp import JobStates as States
[ee8e6d0]4import logging
[33ea505]5import os
[d04a689]6
7# initialize logger
8logger = logging.getLogger(__name__)
[776a659]9
[33ea505]10class GutenbachJob(object):
[b2e077a]11
[345c476]12    def __init__(self, job_id=None, creator=None, name=None,
13                 priority=None, document=None):
[b01b6d1]14        """Create an empty Gutenbach job.
[e58af05]15
[ef387cf]16        Parameters
17        ----------
18        job_id : integer
19            A unique id for this job.
20        creator : string
21            The user creating the job.
22        name : string
23            The human-readable name of the job.
24        priority : integer
25            The priority of the job, used for ordering.
26        document : file object
27            A file object containing the job data.
28
[776a659]29        """
[ee8e6d0]30
[b01b6d1]31        self.player = None
[345c476]32        self.document = None
[ee8e6d0]33
[b01b6d1]34        self.id = job_id
35        self.creator = creator
36        self.name = name
[345c476]37        self.priority = priority
[ef387cf]38       
[2620618]39        self._why_done = None
[345c476]40
41        if document is not None:
42            self.spool(document)
[ee8e6d0]43
[b01b6d1]44    def __repr__(self):
45        return str(self)
46
47    def __str__(self):
48        return "<Job %d '%s'>" % (self.id, self.name)
[e58af05]49
[eee389a]50    def __cmp__(self, other):
[ef387cf]51        """Compares two GutenbachJobs based on their priorities.
52
53        """
[eee389a]54        return cmp(self.priority, other.priority)
55
[57bc2dc]56    def __del__(self):
57        if self.player:
[bd5bffc]58            if self.player.is_playing:
59                self.player.mplayer_stop()
[ab7c1dd]60            if self.player.fh:
61                if self.player.fh.closed:
62                    self.player.fh.close()
[57bc2dc]63            self.player = None
64
[ab7c1dd]65        self.document = None
[57bc2dc]66        self.id = None
67        self.creator = None
68        self.name = None
69        self.priority = None
70        self._why_done = None
71
[b01b6d1]72    ######################################################################
73    ###                          Properties                            ###
74    ######################################################################
[1a63bf7]75
[b01b6d1]76    @property
77    def id(self):
[ef387cf]78        """Unique job identifier (integer).  Should be a positive
79        integer, except when unassigned, when it defaults to -1.
[b01b6d1]80       
81        """
82        return self._id
83    @id.setter
84    def id(self, val):
[1a63bf7]85        try:
[345c476]86            self._id = max(int(val), -1)
[951ab1b]87        except:
[b01b6d1]88            self._id = -1
89
90    @property
[345c476]91    def priority(self):
[ef387cf]92        """Job priority (integer).  Should be a nonzero positive
93        integer; defaults to 1 when unassigned.
94
95        """
[345c476]96        return self._priority
97    @priority.setter
98    def priority(self, val):
99        try:
100            self._priority = max(int(val), 1)
[951ab1b]101        except:
[345c476]102            self._priority = 1
103
104    @property
[b01b6d1]105    def creator(self):
[ef387cf]106        """The user who created the job (string).  Defaults to an
107        empty string.
[b01b6d1]108
109        """
110        return self._creator
111    @creator.setter
112    def creator(self, val):
113        if val is None:
114            self._creator = ""
115        else:
116            self._creator = str(val)
117
118    @property
119    def name(self):
[ef387cf]120        """The job's human-readable name (string).  Defaults to an
121        empty string.
[b01b6d1]122
123        """
124        return self._name
125    @name.setter
126    def name(self, val):
127        if val is None:
128            self._name = ""
129        else:
130            self._name = str(val)
131
132    @property
133    def size(self):
[ef387cf]134        """The size of the job in octets/bytes (integer).  Defaults to
135        0 if no document is specified or if there is an error reading
136        the document.
[1a63bf7]137
[b01b6d1]138        """
[ee8e6d0]139        try:
[33ea505]140            size = os.path.getsize(self.document)
141        except:
142            size = 0
143        return size
[b01b6d1]144
[951ab1b]145    ######################################################################
146    ###                            State                               ###
147    ######################################################################
148
[eee389a]149    @property
[345c476]150    def is_valid(self):
151        """Whether the job is ready to be manipulated (spooled,
152        played, etc).  Note that playing the job still requires it to
153        be spooled first.
154
155        """
[ef387cf]156
[345c476]157        return self.id > 0 and \
158               self.priority > 0
[eee389a]159
160    @property
161    def is_ready(self):
[ef387cf]162        """Whether the job is ready to be played; i.e., it has all the
163        necessary data to actually play the audio data.
[345c476]164
165        """
[ef387cf]166
[345c476]167        return self.is_valid and \
168               self.player is not None and \
169               not self.player.is_playing and \
[97f20dd]170               not self._why_done == "canceled" and \
[345c476]171               not self._why_done == "aborted"
[eee389a]172
173    @property
[345c476]174    def is_playing(self):
175        """Whether the job is currently playing (regardless of whether
[3c0760f]176        it's Paused).
[345c476]177
178        """
[ef387cf]179
[345c476]180        return self.is_valid and \
181               self.player is not None and \
182               self.player.is_playing
183
184    @property
185    def is_paused(self):
186        """Whether the job is currently paused.
187
188        """
[ef387cf]189
[345c476]190        return self.is_valid and \
191               self.player is not None and \
[3c0760f]192               self.player.paused and \
193               self.player.is_playing
[345c476]194
195    @property
196    def is_done(self):
[951ab1b]197        """Whether the job is done playing, regardless of whether it
198        completed successfully or not.
199
200        """
[ef387cf]201
[345c476]202        return (self.is_valid and \
203                self.player is not None and \
[3c0760f]204                self.player.done) or \
[97f20dd]205                (self._why_done == "canceled" or \
[345c476]206                 self._why_done == "aborted")
207
[3c0760f]208    def wait_done(self):
209        self.player.wait_done()
210
[345c476]211    @property
[951ab1b]212    def is_completed(self):
213        """Whether the job completed successfully.
214
215        """
[ef387cf]216
[951ab1b]217        return self.is_done and self._why_done == "completed"
218
219    @property
[97f20dd]220    def is_canceled(self):
221        """Whether the job was canceled.
[951ab1b]222
223        """
[ef387cf]224
[97f20dd]225        return self.is_done and self._why_done == "canceled"
[951ab1b]226
227    @property
228    def is_aborted(self):
229        """Whether the job was aborted.
230
231        """
[ef387cf]232
[951ab1b]233        return self.is_done and self._why_done == "aborted"
234
235    @property
[345c476]236    def state(self):
[2834b9a]237        """RFC 2911: 4.3.7 job-state (type1 enum)
238
239        'pending': The job is a candidate to start processing, but is
240            not yet processing.
241
242        'pending-held': The job is not a candidate for processing for
243            any number of reasons but will return to the 'pending'
244            state as soon as the reasons are no longer present. The
245            job's 'job-state-reason' attribute MUST indicate why the
246            job is no longer a candidate for processing.
247
248        'processing': One or more of:
[2620618]249       
[2834b9a]250            1. the job is using, or is attempting to use, one or more
251               purely software processes that are analyzing, creating,
252               or interpreting a PDL, etc.,
253            2. the job is using, or is attempting to use, one or more
254               hardware devices that are interpreting a PDL, making
255               marks on a medium, and/or performing finishing, such as
256               stapling, etc.,
257            3. the Printer object has made the job ready for printing,
258               but the output device is not yet printing it, either
259               because the job hasn't reached the output device or
260               because the job is queued in the output device or some
261               other spooler, awaiting the output device to print it.
262
263            When the job is in the 'processing' state, the entire job
264            state includes the detailed status represented in the
265            Printer object's 'printer-state', 'printer-state-
266            reasons', and 'printer-state-message' attributes.
267
268            Implementations MAY, though they NEED NOT, include
269            additional values in the job's 'job-state-reasons'
270            attribute to indicate the progress of the job, such as
271            adding the 'job-printing' value to indicate when the
272            output device is actually making marks on paper and/or the
273            'processing-to-stop-point' value to indicate that the IPP
274            object is in the process of canceling or aborting the
275            job. Most implementations won't bother with this nuance.
276
277        'processing-stopped': The job has stopped while processing for
278            any number of reasons and will return to the 'processing'
279            state as soon as the reasons are no longer present.
280
281            The job's 'job-state-reason' attribute MAY indicate why
282            the job has stopped processing. For example, if the output
283            device is stopped, the 'printer-stopped' value MAY be
284            included in the job's 'job-state-reasons' attribute.
285
286            Note: When an output device is stopped, the device usually
287            indicates its condition in human readable form locally at
288            the device. A client can obtain more complete device
289            status remotely by querying the Printer object's
290            'printer-state', 'printer-state-reasons' and 'printer-
291            state-message' attributes.
292
293        'canceled': The job has been canceled by a Cancel-Job
294            operation and the Printer object has completed canceling
295            the job and all job status attributes have reached their
296            final values for the job. While the Printer object is
297            canceling the job, the job remains in its current state,
298            but the job's 'job-state-reasons' attribute SHOULD contain
299            the 'processing-to-stop-point' value and one of the
300            'canceled-by-user', 'canceled-by-operator', or
301            'canceled-at-device' value. When the job moves to the
302            'canceled' state, the 'processing-to-stop-point' value, if
303            present, MUST be removed, but the 'canceled-by-xxx', if
304            present, MUST remain.
305
306        'aborted': The job has been aborted by the system, usually
307            while the job was in the 'processing' or 'processing-
308            stopped' state and the Printer has completed aborting the
309            job and all job status attributes have reached their final
310            values for the job. While the Printer object is aborting
311            the job, the job remains in its current state, but the
312            job's 'job-state-reasons' attribute SHOULD contain the
313            'processing-to-stop-point' and 'aborted-by- system'
314            values. When the job moves to the 'aborted' state, the
315            'processing-to-stop-point' value, if present, MUST be
316            removed, but the 'aborted-by-system' value, if present,
317            MUST remain.
318
319        'completed': The job has completed successfully or with
320            warnings or errors after processing and all of the job
321            media sheets have been successfully stacked in the
322            appropriate output bin(s) and all job status attributes
323            have reached their final values for the job. The job's
324            'job-state-reasons' attribute SHOULD contain one of:
325            'completed-successfully', 'completed-with-warnings', or
326            'completed-with-errors' values.
327
328        The final value for this attribute MUST be one of:
329        'completed', 'canceled', or 'aborted' before the Printer
330        removes the job altogether. The length of time that jobs
331        remain in the 'canceled', 'aborted', and 'completed' states
332        depends on implementation. See section 4.3.7.2.
333
334        The following figure shows the normal job state transitions.
335       
336                                                           +----> canceled
337                                                          /
338            +----> pending --------> processing ---------+------> completed
339            |         ^                   ^               \
340        --->+         |                   |                +----> aborted
341            |         v                   v               /
342            +----> pending-held    processing-stopped ---+
343
344        Normally a job progresses from left to right. Other state
345        transitions are unlikely, but are not forbidden. Not shown are
346        the transitions to the 'canceled' state from the 'pending',
347        'pending- held', and 'processing-stopped' states.
348
349        Jobs reach one of the three terminal states: 'completed',
350        'canceled', or 'aborted', after the jobs have completed all
351        activity, including stacking output media, after the jobs have
352        completed all activity, and all job status attributes have
353        reached their final values for the job.
354
[a2b0582]355        """
[ef387cf]356
[2834b9a]357        # XXX verify that these transitions are correct!
358
[345c476]359        if self.is_ready:
360            state = States.PENDING
361        elif self.is_playing and not self.is_paused:
362            state = States.PROCESSING
363        elif self.is_playing and self.is_paused:
[97f20dd]364            state = States.PROCESSING_STOPPED
[951ab1b]365        elif self.is_completed:
[97f20dd]366            state = States.COMPLETED
367        elif self.is_canceled:
368            state = States.CANCELED
[951ab1b]369        elif self.is_aborted:
[345c476]370            state = States.ABORTED
371        else:
[97f20dd]372            state = States.PENDING_HELD
[345c476]373        return state
374
[b01b6d1]375    ######################################################################
376    ###                            Methods                             ###
377    ######################################################################
378
[951ab1b]379    @staticmethod
380    def verify_document(document):
[ef387cf]381        """Verifies that a document has the 'name', 'read', and
382        'close' attributes (i.e., it should be like a file object).
383
384        """
385       
[951ab1b]386        if not hasattr(document, "name"):
387            raise errors.InvalidDocument, "no name attribute"
388        if not hasattr(document, "read"):
389            raise errors.InvalidDocument, "no read attribute"
390        if not hasattr(document, "close"):
391            raise errors.InvalidDocument, "no close attribute"
[57bc2dc]392        if not hasattr(document, "closed"):
393            raise errors.InvalidDocument, "no closed attribute"
[951ab1b]394
395    def spool(self, document=None):
[ef387cf]396        """Non-blocking spool.  Job must be valid (see
397        'GutenbachJob.is_valid'), and the document must be an open
398        file handler.
[951ab1b]399
400        Raises
401        ------
402        InvalidDocument
403            If the document is not valid.
404        InvalidJobStateException
405            If the job is not valid or it is already
406            spooled/ready/finished.
407
408        """
409
[97f20dd]410        if not self.is_valid or self.state != States.PENDING_HELD:
[951ab1b]411            raise errors.InvalidJobStateException(self.state)
412        self.verify_document(document)
[33ea505]413        self.document = document.name
414        self.player = Player(document)
415        logger.debug("document for job %d is '%s'" % (self.id, self.document))
416
[b01b6d1]417    def play(self):
[ef387cf]418        """Non-blocking play.  Job must be ready (see
419        'GutenbachJob.is_ready').
[33ea505]420
421        Raises
422        ------
423        InvalidJobStateException
424            If the job is not ready to be played.
[eee389a]425
426        """
427       
428        # make sure the job is waiting to be played and that it's
429        # valid
[33ea505]430        if not self.is_ready:
[951ab1b]431            raise errors.InvalidJobStateException(self.state)
[eee389a]432       
433        # and set the state to processing if we're good to go
[b01b6d1]434        logger.info("playing job %s" % str(self))
[33ea505]435
436        def _completed():
437            logger.info("completed job %s" % str(self))
[345c476]438            self._why_done = "completed"
[33ea505]439        self.player.callback = _completed
[d21198f]440        self.player.start()
[eee389a]441
442    def pause(self):
[ef387cf]443        """Non-blocking pause.  Job must be playing (see
444        'GutenbachJob.is_playing').
[951ab1b]445
446        Raises
447        ------
448        InvalidJobStateException
449            If the job is not playing.
[33ea505]450
451        """
452       
453        if not self.is_playing:
[951ab1b]454            raise errors.InvalidJobStateException(self.state)
[33ea505]455        self.player.mplayer_pause()
[34a4e5d]456
[cb0195f]457    def resume(self):
458        """Non-blocking resume.  Job must be paused (see
459        'GutenbachJob.is_paused').
460
461        Raises
462        ------
463        InvalidJobStateException
464            If the job is not paused.
465
466        """
467        if not self.is_paused:
468            raise errors.InvalidJobStateException(self.state)
469        self.player.mplayer_pause()
470
[34a4e5d]471    def cancel(self):
[b8c3505]472        """Blocking cancel. The job must not have been previously
[ef387cf]473        aborted or completed (though this method will succeed if it
[97f20dd]474        was previously canceled).  This should be used to stop the
[ef387cf]475        job following an external request.
[951ab1b]476
477        Raises
478        ------
479        InvalidJobStateException
480            If the job has already finished.
481
482        """
483       
[ca1cfa9]484        if self.player and self.player.is_playing:
[b8c3505]485            self.player._callback = None
[34a4e5d]486            self.player.mplayer_stop()
[b8c3505]487
[97f20dd]488        elif self.is_done and not self._why_done == "canceled":
[951ab1b]489            raise errors.InvalidJobStateException(self.state)
[b8c3505]490
[97f20dd]491        logger.info("canceled job %s" % str(self))
492        self._why_done = "canceled"
[eee389a]493
[34a4e5d]494    def abort(self):
[b8c3505]495        """Blocking abort. The job must not have been previously
[97f20dd]496        canceled or completed (though this method will succeed if it
[ef387cf]497        was previously aborted).  This should be used to stop the job
498        following internal errors.
[951ab1b]499
500        Raises
501        ------
502        InvalidJobStateException
503            If the job has already finished.
504
505        """
506
[ca1cfa9]507        if self.player and self.player.is_playing:
[b8c3505]508            self.player._callback = None
[eee389a]509            self.player.mplayer_stop()
[b8c3505]510
[345c476]511        elif self.is_done and not self._why_done == "aborted":
[951ab1b]512            raise errors.InvalidJobStateException(self.state)
[b8c3505]513
514        logger.info("aborted job %s" % str(self))
515        self._why_done = "aborted"
[776a659]516
[609a9b0]517    def restart(self):
[c1cebbc]518        """Non-blocking restart.  Job must be finished (see
519        'GutenbachJob.is_done'), and will be ready to be played (see
520        'GutenbachJob.is_ready') if this method is successful.
521
522        Raises
523        ------
524        InvalidJobStateException
525            If the job is not done.
526
527        """
528
529        if not self.is_done:
530            raise errors.InvalidJobStateException(self.state)
531
[0494098]532        logger.debug("restarting job %d", self.id)
[c1cebbc]533
534        self._why_done = None
[0494098]535        fh = self.player.fh
536
537        if not fh or fh.closed:
538            raise RuntimeError, "file handler is closed"
539
540        self.player = Player(fh)
Note: See TracBrowser for help on using the repository browser.