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
Line 
1from . import errors
2from .player import Player
3from gutenbach.ipp import JobStates as States
4import logging
5import os
6
7# initialize logger
8logger = logging.getLogger(__name__)
9
10class GutenbachJob(object):
11
12    def __init__(self, job_id=None, creator=None, name=None,
13                 priority=None, document=None):
14        """Create an empty Gutenbach job.
15
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
29        """
30
31        self.player = None
32        self.document = None
33
34        self.id = job_id
35        self.creator = creator
36        self.name = name
37        self.priority = priority
38       
39        self._why_done = None
40
41        if document is not None:
42            self.spool(document)
43
44    def __repr__(self):
45        return str(self)
46
47    def __str__(self):
48        return "<Job %d '%s'>" % (self.id, self.name)
49
50    def __cmp__(self, other):
51        """Compares two GutenbachJobs based on their priorities.
52
53        """
54        return cmp(self.priority, other.priority)
55
56    def __del__(self):
57        if self.player:
58            if self.player.is_playing:
59                self.player.mplayer_stop()
60            if self.player.fh:
61                if self.player.fh.closed:
62                    self.player.fh.close()
63            self.player = None
64
65        self.document = None
66        self.id = None
67        self.creator = None
68        self.name = None
69        self.priority = None
70        self._why_done = None
71
72    ######################################################################
73    ###                          Properties                            ###
74    ######################################################################
75
76    @property
77    def id(self):
78        """Unique job identifier (integer).  Should be a positive
79        integer, except when unassigned, when it defaults to -1.
80       
81        """
82        return self._id
83    @id.setter
84    def id(self, val):
85        try:
86            self._id = max(int(val), -1)
87        except:
88            self._id = -1
89
90    @property
91    def priority(self):
92        """Job priority (integer).  Should be a nonzero positive
93        integer; defaults to 1 when unassigned.
94
95        """
96        return self._priority
97    @priority.setter
98    def priority(self, val):
99        try:
100            self._priority = max(int(val), 1)
101        except:
102            self._priority = 1
103
104    @property
105    def creator(self):
106        """The user who created the job (string).  Defaults to an
107        empty string.
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):
120        """The job's human-readable name (string).  Defaults to an
121        empty string.
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):
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.
137
138        """
139        try:
140            size = os.path.getsize(self.document)
141        except:
142            size = 0
143        return size
144
145    ######################################################################
146    ###                            State                               ###
147    ######################################################################
148
149    @property
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        """
156
157        return self.id > 0 and \
158               self.priority > 0
159
160    @property
161    def is_ready(self):
162        """Whether the job is ready to be played; i.e., it has all the
163        necessary data to actually play the audio data.
164
165        """
166
167        return self.is_valid and \
168               self.player is not None and \
169               not self.player.is_playing and \
170               not self._why_done == "canceled" and \
171               not self._why_done == "aborted"
172
173    @property
174    def is_playing(self):
175        """Whether the job is currently playing (regardless of whether
176        it's Paused).
177
178        """
179
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        """
189
190        return self.is_valid and \
191               self.player is not None and \
192               self.player.paused and \
193               self.player.is_playing
194
195    @property
196    def is_done(self):
197        """Whether the job is done playing, regardless of whether it
198        completed successfully or not.
199
200        """
201
202        return (self.is_valid and \
203                self.player is not None and \
204                self.player.done) or \
205                (self._why_done == "canceled" or \
206                 self._why_done == "aborted")
207
208    def wait_done(self):
209        self.player.wait_done()
210
211    @property
212    def is_completed(self):
213        """Whether the job completed successfully.
214
215        """
216
217        return self.is_done and self._why_done == "completed"
218
219    @property
220    def is_canceled(self):
221        """Whether the job was canceled.
222
223        """
224
225        return self.is_done and self._why_done == "canceled"
226
227    @property
228    def is_aborted(self):
229        """Whether the job was aborted.
230
231        """
232
233        return self.is_done and self._why_done == "aborted"
234
235    @property
236    def state(self):
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:
249       
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
355        """
356
357        # XXX verify that these transitions are correct!
358
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:
364            state = States.PROCESSING_STOPPED
365        elif self.is_completed:
366            state = States.COMPLETED
367        elif self.is_canceled:
368            state = States.CANCELED
369        elif self.is_aborted:
370            state = States.ABORTED
371        else:
372            state = States.PENDING_HELD
373        return state
374
375    ######################################################################
376    ###                            Methods                             ###
377    ######################################################################
378
379    @staticmethod
380    def verify_document(document):
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       
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"
392        if not hasattr(document, "closed"):
393            raise errors.InvalidDocument, "no closed attribute"
394
395    def spool(self, document=None):
396        """Non-blocking spool.  Job must be valid (see
397        'GutenbachJob.is_valid'), and the document must be an open
398        file handler.
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
410        if not self.is_valid or self.state != States.PENDING_HELD:
411            raise errors.InvalidJobStateException(self.state)
412        self.verify_document(document)
413        self.document = document.name
414        self.player = Player(document)
415        logger.debug("document for job %d is '%s'" % (self.id, self.document))
416
417    def play(self):
418        """Non-blocking play.  Job must be ready (see
419        'GutenbachJob.is_ready').
420
421        Raises
422        ------
423        InvalidJobStateException
424            If the job is not ready to be played.
425
426        """
427       
428        # make sure the job is waiting to be played and that it's
429        # valid
430        if not self.is_ready:
431            raise errors.InvalidJobStateException(self.state)
432       
433        # and set the state to processing if we're good to go
434        logger.info("playing job %s" % str(self))
435
436        def _completed():
437            logger.info("completed job %s" % str(self))
438            self._why_done = "completed"
439        self.player.callback = _completed
440        self.player.start()
441
442    def pause(self):
443        """Non-blocking pause.  Job must be playing (see
444        'GutenbachJob.is_playing').
445
446        Raises
447        ------
448        InvalidJobStateException
449            If the job is not playing.
450
451        """
452       
453        if not self.is_playing:
454            raise errors.InvalidJobStateException(self.state)
455        self.player.mplayer_pause()
456
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
471    def cancel(self):
472        """Blocking cancel. The job must not have been previously
473        aborted or completed (though this method will succeed if it
474        was previously canceled).  This should be used to stop the
475        job following an external request.
476
477        Raises
478        ------
479        InvalidJobStateException
480            If the job has already finished.
481
482        """
483       
484        if self.player and self.player.is_playing:
485            self.player._callback = None
486            self.player.mplayer_stop()
487
488        elif self.is_done and not self._why_done == "canceled":
489            raise errors.InvalidJobStateException(self.state)
490
491        logger.info("canceled job %s" % str(self))
492        self._why_done = "canceled"
493
494    def abort(self):
495        """Blocking abort. The job must not have been previously
496        canceled or completed (though this method will succeed if it
497        was previously aborted).  This should be used to stop the job
498        following internal errors.
499
500        Raises
501        ------
502        InvalidJobStateException
503            If the job has already finished.
504
505        """
506
507        if self.player and self.player.is_playing:
508            self.player._callback = None
509            self.player.mplayer_stop()
510
511        elif self.is_done and not self._why_done == "aborted":
512            raise errors.InvalidJobStateException(self.state)
513
514        logger.info("aborted job %s" % str(self))
515        self._why_done = "aborted"
516
517    def restart(self):
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
532        logger.debug("restarting job %d", self.id)
533
534        self._why_done = None
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.