source: server/lib/gutenbach/server/job.py @ 441604f

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

Convert a few tabs to spaces.

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