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

no-cups
Last change on this file since ab7c1dd was ab7c1dd, checked in by Jessica B. Hamrick <jhamrick@…>, 12 years ago

Fix job destructor

  • Property mode set to 100644
File size: 11.6 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            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 == "cancelled" 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 == "cancelled" 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_cancelled(self):
216        """Whether the job was cancelled.
217
218        """
219
220        return self.is_done and self._why_done == "cancelled"
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        """State status codes (these are equivalent to the IPP
233        job-state status codes).  State transitions are as follows:
234       
235            HELD ---> PENDING ---> PROCESSING <--> STOPPED (aka paused)
236                         ^              |---> CANCELLED
237                         |              |---> ABORTED
238                         |              |---> COMPLETE ---|
239                         |--------------------------------|
240                     
241        """
242
243        if self.is_ready:
244            state = States.PENDING
245        elif self.is_playing and not self.is_paused:
246            state = States.PROCESSING
247        elif self.is_playing and self.is_paused:
248            state = States.STOPPED
249        elif self.is_completed:
250            state = States.COMPLETE
251        elif self.is_cancelled:
252            state = States.CANCELLED
253        elif self.is_aborted:
254            state = States.ABORTED
255        else:
256            state = States.HELD
257        return state
258
259    ######################################################################
260    ###                            Methods                             ###
261    ######################################################################
262
263    @staticmethod
264    def verify_document(document):
265        """Verifies that a document has the 'name', 'read', and
266        'close' attributes (i.e., it should be like a file object).
267
268        """
269       
270        if not hasattr(document, "name"):
271            raise errors.InvalidDocument, "no name attribute"
272        if not hasattr(document, "read"):
273            raise errors.InvalidDocument, "no read attribute"
274        if not hasattr(document, "close"):
275            raise errors.InvalidDocument, "no close attribute"
276        if not hasattr(document, "closed"):
277            raise errors.InvalidDocument, "no closed attribute"
278
279    def spool(self, document=None):
280        """Non-blocking spool.  Job must be valid (see
281        'GutenbachJob.is_valid'), and the document must be an open
282        file handler.
283
284        Raises
285        ------
286        InvalidDocument
287            If the document is not valid.
288        InvalidJobStateException
289            If the job is not valid or it is already
290            spooled/ready/finished.
291
292        """
293
294        if not self.is_valid or self.state != States.HELD:
295            raise errors.InvalidJobStateException(self.state)
296        self.verify_document(document)
297        self.document = document.name
298        self.player = Player(document)
299        logger.debug("document for job %d is '%s'" % (self.id, self.document))
300
301    def play(self):
302        """Non-blocking play.  Job must be ready (see
303        'GutenbachJob.is_ready').
304
305        Raises
306        ------
307        InvalidJobStateException
308            If the job is not ready to be played.
309
310        """
311       
312        # make sure the job is waiting to be played and that it's
313        # valid
314        if not self.is_ready:
315            raise errors.InvalidJobStateException(self.state)
316       
317        # and set the state to processing if we're good to go
318        logger.info("playing job %s" % str(self))
319
320        def _completed():
321            logger.info("completed job %s" % str(self))
322            self._why_done = "completed"
323        self.player.callback = _completed
324        self.player.start()
325
326    def pause(self):
327        """Non-blocking pause.  Job must be playing (see
328        'GutenbachJob.is_playing').
329
330        Raises
331        ------
332        InvalidJobStateException
333            If the job is not playing.
334
335        """
336       
337        if not self.is_playing:
338            raise errors.InvalidJobStateException(self.state)
339        self.player.mplayer_pause()
340
341    def resume(self):
342        """Non-blocking resume.  Job must be paused (see
343        'GutenbachJob.is_paused').
344
345        Raises
346        ------
347        InvalidJobStateException
348            If the job is not paused.
349
350        """
351        if not self.is_paused:
352            raise errors.InvalidJobStateException(self.state)
353        self.player.mplayer_pause()
354
355    def cancel(self):
356        """Blocking cancel. The job must not have been previously
357        aborted or completed (though this method will succeed if it
358        was previously cancelled).  This should be used to stop the
359        job following an external request.
360
361        Raises
362        ------
363        InvalidJobStateException
364            If the job has already finished.
365
366        """
367       
368        if self.is_playing:
369            self.player._callback = None
370            self.player.mplayer_stop()
371
372        elif self.is_done and not self._why_done == "cancelled":
373            raise errors.InvalidJobStateException(self.state)
374
375        logger.info("cancelled job %s" % str(self))
376        self._why_done = "cancelled"
377
378    def abort(self):
379        """Blocking abort. The job must not have been previously
380        cancelled or completed (though this method will succeed if it
381        was previously aborted).  This should be used to stop the job
382        following internal errors.
383
384        Raises
385        ------
386        InvalidJobStateException
387            If the job has already finished.
388
389        """
390
391        if self.is_playing:
392            self.player._callback = None
393            self.player.mplayer_stop()
394
395        elif self.is_done and not self._why_done == "aborted":
396            raise errors.InvalidJobStateException(self.state)
397
398        logger.info("aborted job %s" % str(self))
399        self._why_done = "aborted"
400
401    def restart(self):
402        """Non-blocking restart.  Job must be finished (see
403        'GutenbachJob.is_done'), and will be ready to be played (see
404        'GutenbachJob.is_ready') if this method is successful.
405
406        Raises
407        ------
408        InvalidJobStateException
409            If the job is not done.
410
411        """
412
413        if not self.is_done:
414            raise errors.InvalidJobStateException(self.state)
415
416        logger.debug("restarting job %d" % (self.id, self.document))
417
418        self._why_done = None
419        self.spool(self.document)
Note: See TracBrowser for help on using the repository browser.