source: server/lib/gutenbach/server/job.py @ ca1cfa9

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

Ensure that the player is stopped if a job is canceled or aborted.

Instead of checking job.is_playing, check player.is_playing. We do this
in case of a mismatch (to prevent runnaway mplayer jobs).

  • Property mode set to 100644
File size: 17.7 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.is_paused       
193
194    @property
195    def is_done(self):
196        """Whether the job is done playing, regardless of whether it
197        completed successfully or not.
198
199        """
200
201        return (self.is_valid and \
202                self.player is not None and \
203                self.player.is_done) or \
204                (self._why_done == "canceled" or \
205                 self._why_done == "aborted")
206
207    @property
208    def is_completed(self):
209        """Whether the job completed successfully.
210
211        """
212
213        return self.is_done and self._why_done == "completed"
214
215    @property
216    def is_canceled(self):
217        """Whether the job was canceled.
218
219        """
220
221        return self.is_done and self._why_done == "canceled"
222
223    @property
224    def is_aborted(self):
225        """Whether the job was aborted.
226
227        """
228
229        return self.is_done and self._why_done == "aborted"
230
231    @property
232    def state(self):
233        """RFC 2911: 4.3.7 job-state (type1 enum)
234
235        'pending': The job is a candidate to start processing, but is
236            not yet processing.
237
238        'pending-held': The job is not a candidate for processing for
239            any number of reasons but will return to the 'pending'
240            state as soon as the reasons are no longer present. The
241            job's 'job-state-reason' attribute MUST indicate why the
242            job is no longer a candidate for processing.
243
244        'processing': One or more of:
245       
246            1. the job is using, or is attempting to use, one or more
247               purely software processes that are analyzing, creating,
248               or interpreting a PDL, etc.,
249            2. the job is using, or is attempting to use, one or more
250               hardware devices that are interpreting a PDL, making
251               marks on a medium, and/or performing finishing, such as
252               stapling, etc.,
253            3. the Printer object has made the job ready for printing,
254               but the output device is not yet printing it, either
255               because the job hasn't reached the output device or
256               because the job is queued in the output device or some
257               other spooler, awaiting the output device to print it.
258
259            When the job is in the 'processing' state, the entire job
260            state includes the detailed status represented in the
261            Printer object's 'printer-state', 'printer-state-
262            reasons', and 'printer-state-message' attributes.
263
264            Implementations MAY, though they NEED NOT, include
265            additional values in the job's 'job-state-reasons'
266            attribute to indicate the progress of the job, such as
267            adding the 'job-printing' value to indicate when the
268            output device is actually making marks on paper and/or the
269            'processing-to-stop-point' value to indicate that the IPP
270            object is in the process of canceling or aborting the
271            job. Most implementations won't bother with this nuance.
272
273        'processing-stopped': The job has stopped while processing for
274            any number of reasons and will return to the 'processing'
275            state as soon as the reasons are no longer present.
276
277            The job's 'job-state-reason' attribute MAY indicate why
278            the job has stopped processing. For example, if the output
279            device is stopped, the 'printer-stopped' value MAY be
280            included in the job's 'job-state-reasons' attribute.
281
282            Note: When an output device is stopped, the device usually
283            indicates its condition in human readable form locally at
284            the device. A client can obtain more complete device
285            status remotely by querying the Printer object's
286            'printer-state', 'printer-state-reasons' and 'printer-
287            state-message' attributes.
288
289        'canceled': The job has been canceled by a Cancel-Job
290            operation and the Printer object has completed canceling
291            the job and all job status attributes have reached their
292            final values for the job. While the Printer object is
293            canceling the job, the job remains in its current state,
294            but the job's 'job-state-reasons' attribute SHOULD contain
295            the 'processing-to-stop-point' value and one of the
296            'canceled-by-user', 'canceled-by-operator', or
297            'canceled-at-device' value. When the job moves to the
298            'canceled' state, the 'processing-to-stop-point' value, if
299            present, MUST be removed, but the 'canceled-by-xxx', if
300            present, MUST remain.
301
302        'aborted': The job has been aborted by the system, usually
303            while the job was in the 'processing' or 'processing-
304            stopped' state and the Printer has completed aborting the
305            job and all job status attributes have reached their final
306            values for the job. While the Printer object is aborting
307            the job, the job remains in its current state, but the
308            job's 'job-state-reasons' attribute SHOULD contain the
309            'processing-to-stop-point' and 'aborted-by- system'
310            values. When the job moves to the 'aborted' state, the
311            'processing-to-stop-point' value, if present, MUST be
312            removed, but the 'aborted-by-system' value, if present,
313            MUST remain.
314
315        'completed': The job has completed successfully or with
316            warnings or errors after processing and all of the job
317            media sheets have been successfully stacked in the
318            appropriate output bin(s) and all job status attributes
319            have reached their final values for the job. The job's
320            'job-state-reasons' attribute SHOULD contain one of:
321            'completed-successfully', 'completed-with-warnings', or
322            'completed-with-errors' values.
323
324        The final value for this attribute MUST be one of:
325        'completed', 'canceled', or 'aborted' before the Printer
326        removes the job altogether. The length of time that jobs
327        remain in the 'canceled', 'aborted', and 'completed' states
328        depends on implementation. See section 4.3.7.2.
329
330        The following figure shows the normal job state transitions.
331       
332                                                           +----> canceled
333                                                          /
334            +----> pending --------> processing ---------+------> completed
335            |         ^                   ^               \
336        --->+         |                   |                +----> aborted
337            |         v                   v               /
338            +----> pending-held    processing-stopped ---+
339
340        Normally a job progresses from left to right. Other state
341        transitions are unlikely, but are not forbidden. Not shown are
342        the transitions to the 'canceled' state from the 'pending',
343        'pending- held', and 'processing-stopped' states.
344
345        Jobs reach one of the three terminal states: 'completed',
346        'canceled', or 'aborted', after the jobs have completed all
347        activity, including stacking output media, after the jobs have
348        completed all activity, and all job status attributes have
349        reached their final values for the job.
350
351        """
352
353        # XXX verify that these transitions are correct!
354
355        if self.is_ready:
356            state = States.PENDING
357        elif self.is_playing and not self.is_paused:
358            state = States.PROCESSING
359        elif self.is_playing and self.is_paused:
360            state = States.PROCESSING_STOPPED
361        elif self.is_completed:
362            state = States.COMPLETED
363        elif self.is_canceled:
364            state = States.CANCELED
365        elif self.is_aborted:
366            state = States.ABORTED
367        else:
368            state = States.PENDING_HELD
369        return state
370
371    ######################################################################
372    ###                            Methods                             ###
373    ######################################################################
374
375    @staticmethod
376    def verify_document(document):
377        """Verifies that a document has the 'name', 'read', and
378        'close' attributes (i.e., it should be like a file object).
379
380        """
381       
382        if not hasattr(document, "name"):
383            raise errors.InvalidDocument, "no name attribute"
384        if not hasattr(document, "read"):
385            raise errors.InvalidDocument, "no read attribute"
386        if not hasattr(document, "close"):
387            raise errors.InvalidDocument, "no close attribute"
388        if not hasattr(document, "closed"):
389            raise errors.InvalidDocument, "no closed attribute"
390
391    def spool(self, document=None):
392        """Non-blocking spool.  Job must be valid (see
393        'GutenbachJob.is_valid'), and the document must be an open
394        file handler.
395
396        Raises
397        ------
398        InvalidDocument
399            If the document is not valid.
400        InvalidJobStateException
401            If the job is not valid or it is already
402            spooled/ready/finished.
403
404        """
405
406        if not self.is_valid or self.state != States.PENDING_HELD:
407            raise errors.InvalidJobStateException(self.state)
408        self.verify_document(document)
409        self.document = document.name
410        self.player = Player(document)
411        logger.debug("document for job %d is '%s'" % (self.id, self.document))
412
413    def play(self):
414        """Non-blocking play.  Job must be ready (see
415        'GutenbachJob.is_ready').
416
417        Raises
418        ------
419        InvalidJobStateException
420            If the job is not ready to be played.
421
422        """
423       
424        # make sure the job is waiting to be played and that it's
425        # valid
426        if not self.is_ready:
427            raise errors.InvalidJobStateException(self.state)
428       
429        # and set the state to processing if we're good to go
430        logger.info("playing job %s" % str(self))
431
432        def _completed():
433            logger.info("completed job %s" % str(self))
434            self._why_done = "completed"
435        self.player.callback = _completed
436        self.player.start()
437
438    def pause(self):
439        """Non-blocking pause.  Job must be playing (see
440        'GutenbachJob.is_playing').
441
442        Raises
443        ------
444        InvalidJobStateException
445            If the job is not playing.
446
447        """
448       
449        if not self.is_playing:
450            raise errors.InvalidJobStateException(self.state)
451        self.player.mplayer_pause()
452
453    def resume(self):
454        """Non-blocking resume.  Job must be paused (see
455        'GutenbachJob.is_paused').
456
457        Raises
458        ------
459        InvalidJobStateException
460            If the job is not paused.
461
462        """
463        if not self.is_paused:
464            raise errors.InvalidJobStateException(self.state)
465        self.player.mplayer_pause()
466
467    def cancel(self):
468        """Blocking cancel. The job must not have been previously
469        aborted or completed (though this method will succeed if it
470        was previously canceled).  This should be used to stop the
471        job following an external request.
472
473        Raises
474        ------
475        InvalidJobStateException
476            If the job has already finished.
477
478        """
479       
480        if self.player and self.player.is_playing:
481            self.player._callback = None
482            self.player.mplayer_stop()
483
484        elif self.is_done and not self._why_done == "canceled":
485            raise errors.InvalidJobStateException(self.state)
486
487        logger.info("canceled job %s" % str(self))
488        self._why_done = "canceled"
489
490    def abort(self):
491        """Blocking abort. The job must not have been previously
492        canceled or completed (though this method will succeed if it
493        was previously aborted).  This should be used to stop the job
494        following internal errors.
495
496        Raises
497        ------
498        InvalidJobStateException
499            If the job has already finished.
500
501        """
502
503        if self.player and self.player.is_playing:
504            self.player._callback = None
505            self.player.mplayer_stop()
506
507        elif self.is_done and not self._why_done == "aborted":
508            raise errors.InvalidJobStateException(self.state)
509
510        logger.info("aborted job %s" % str(self))
511        self._why_done = "aborted"
512
513    def restart(self):
514        """Non-blocking restart.  Job must be finished (see
515        'GutenbachJob.is_done'), and will be ready to be played (see
516        'GutenbachJob.is_ready') if this method is successful.
517
518        Raises
519        ------
520        InvalidJobStateException
521            If the job is not done.
522
523        """
524
525        if not self.is_done:
526            raise errors.InvalidJobStateException(self.state)
527
528        logger.debug("restarting job %d", self.id)
529
530        self._why_done = None
531        fh = self.player.fh
532
533        if not fh or fh.closed:
534            raise RuntimeError, "file handler is closed"
535
536        self.player = Player(fh)
Note: See TracBrowser for help on using the repository browser.