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

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

Better threading with job.py and player.py

  • Property mode set to 100644
File size: 10.3 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
[b01b6d1]56    ######################################################################
57    ###                          Properties                            ###
58    ######################################################################
[1a63bf7]59
[b01b6d1]60    @property
61    def id(self):
[ef387cf]62        """Unique job identifier (integer).  Should be a positive
63        integer, except when unassigned, when it defaults to -1.
[b01b6d1]64       
65        """
66        return self._id
67    @id.setter
68    def id(self, val):
[1a63bf7]69        try:
[345c476]70            self._id = max(int(val), -1)
[951ab1b]71        except:
[b01b6d1]72            self._id = -1
73
74    @property
[345c476]75    def priority(self):
[ef387cf]76        """Job priority (integer).  Should be a nonzero positive
77        integer; defaults to 1 when unassigned.
78
79        """
[345c476]80        return self._priority
81    @priority.setter
82    def priority(self, val):
83        try:
84            self._priority = max(int(val), 1)
[951ab1b]85        except:
[345c476]86            self._priority = 1
87
88    @property
[b01b6d1]89    def creator(self):
[ef387cf]90        """The user who created the job (string).  Defaults to an
91        empty string.
[b01b6d1]92
93        """
94        return self._creator
95    @creator.setter
96    def creator(self, val):
97        if val is None:
98            self._creator = ""
99        else:
100            self._creator = str(val)
101
102    @property
103    def name(self):
[ef387cf]104        """The job's human-readable name (string).  Defaults to an
105        empty string.
[b01b6d1]106
107        """
108        return self._name
109    @name.setter
110    def name(self, val):
111        if val is None:
112            self._name = ""
113        else:
114            self._name = str(val)
115
116    @property
117    def size(self):
[ef387cf]118        """The size of the job in octets/bytes (integer).  Defaults to
119        0 if no document is specified or if there is an error reading
120        the document.
[1a63bf7]121
[b01b6d1]122        """
[ee8e6d0]123        try:
[33ea505]124            size = os.path.getsize(self.document)
125        except:
126            size = 0
127        return size
[b01b6d1]128
[951ab1b]129    ######################################################################
130    ###                            State                               ###
131    ######################################################################
132
[eee389a]133    @property
[345c476]134    def is_valid(self):
135        """Whether the job is ready to be manipulated (spooled,
136        played, etc).  Note that playing the job still requires it to
137        be spooled first.
138
139        """
[ef387cf]140
[345c476]141        return self.id > 0 and \
142               self.priority > 0
[eee389a]143
144    @property
145    def is_ready(self):
[ef387cf]146        """Whether the job is ready to be played; i.e., it has all the
147        necessary data to actually play the audio data.
[345c476]148
149        """
[ef387cf]150
[345c476]151        return self.is_valid and \
152               self.player is not None and \
153               not self.player.is_playing and \
154               not self._why_done == "cancelled" and \
155               not self._why_done == "aborted"
[eee389a]156
157    @property
[345c476]158    def is_playing(self):
159        """Whether the job is currently playing (regardless of whether
160        it's paused).
161
162        """
[ef387cf]163
[345c476]164        return self.is_valid and \
165               self.player is not None and \
166               self.player.is_playing
167
168    @property
169    def is_paused(self):
170        """Whether the job is currently paused.
171
172        """
[ef387cf]173
[345c476]174        return self.is_valid and \
175               self.player is not None and \
176               self.player.is_paused       
177
178    @property
179    def is_done(self):
[951ab1b]180        """Whether the job is done playing, regardless of whether it
181        completed successfully or not.
182
183        """
[ef387cf]184
[345c476]185        return (self.is_valid and \
186                self.player is not None and \
187                self.player.is_done) or \
188                (self._why_done == "cancelled" or \
189                 self._why_done == "aborted")
190
191    @property
[951ab1b]192    def is_completed(self):
193        """Whether the job completed successfully.
194
195        """
[ef387cf]196
[951ab1b]197        return self.is_done and self._why_done == "completed"
198
199    @property
200    def is_cancelled(self):
201        """Whether the job was cancelled.
202
203        """
[ef387cf]204
[951ab1b]205        return self.is_done and self._why_done == "cancelled"
206
207    @property
208    def is_aborted(self):
209        """Whether the job was aborted.
210
211        """
[ef387cf]212
[951ab1b]213        return self.is_done and self._why_done == "aborted"
214
215    @property
[345c476]216    def state(self):
[ef387cf]217        """State status codes (these are equivalent to the IPP
218        job-state status codes).  State transitions are as follows:
[2620618]219       
[ef387cf]220            HELD ---> PENDING ---> PROCESSING <--> STOPPED (aka paused)
221                         ^              |---> CANCELLED
222                         |              |---> ABORTED
223                         |              |---> COMPLETE ---|
224                         |--------------------------------|
[2620618]225                     
[a2b0582]226        """
[ef387cf]227
[345c476]228        if self.is_ready:
229            state = States.PENDING
230        elif self.is_playing and not self.is_paused:
231            state = States.PROCESSING
232        elif self.is_playing and self.is_paused:
233            state = States.STOPPED
[951ab1b]234        elif self.is_completed:
[345c476]235            state = States.COMPLETE
[951ab1b]236        elif self.is_cancelled:
[345c476]237            state = States.CANCELLED
[951ab1b]238        elif self.is_aborted:
[345c476]239            state = States.ABORTED
240        else:
241            state = States.HELD
242        return state
243
[b01b6d1]244    ######################################################################
245    ###                            Methods                             ###
246    ######################################################################
247
[951ab1b]248    @staticmethod
249    def verify_document(document):
[ef387cf]250        """Verifies that a document has the 'name', 'read', and
251        'close' attributes (i.e., it should be like a file object).
252
253        """
254       
[951ab1b]255        if not hasattr(document, "name"):
256            raise errors.InvalidDocument, "no name attribute"
257        if not hasattr(document, "read"):
258            raise errors.InvalidDocument, "no read attribute"
259        if not hasattr(document, "close"):
260            raise errors.InvalidDocument, "no close attribute"
261
262    def spool(self, document=None):
[ef387cf]263        """Non-blocking spool.  Job must be valid (see
264        'GutenbachJob.is_valid'), and the document must be an open
265        file handler.
[951ab1b]266
267        Raises
268        ------
269        InvalidDocument
270            If the document is not valid.
271        InvalidJobStateException
272            If the job is not valid or it is already
273            spooled/ready/finished.
274
275        """
276
277        if not self.is_valid or self.state != States.HELD:
278            raise errors.InvalidJobStateException(self.state)
279        self.verify_document(document)
[33ea505]280        self.document = document.name
281        self.player = Player(document)
282        logger.debug("document for job %d is '%s'" % (self.id, self.document))
283
[b01b6d1]284    def play(self):
[ef387cf]285        """Non-blocking play.  Job must be ready (see
286        'GutenbachJob.is_ready').
[33ea505]287
288        Raises
289        ------
290        InvalidJobStateException
291            If the job is not ready to be played.
[eee389a]292
293        """
294       
295        # make sure the job is waiting to be played and that it's
296        # valid
[33ea505]297        if not self.is_ready:
[951ab1b]298            raise errors.InvalidJobStateException(self.state)
[eee389a]299       
300        # and set the state to processing if we're good to go
[b01b6d1]301        logger.info("playing job %s" % str(self))
[33ea505]302
303        def _completed():
304            logger.info("completed job %s" % str(self))
[345c476]305            self._why_done = "completed"
[33ea505]306        self.player.callback = _completed
[d21198f]307        self.player.start()
[eee389a]308
309    def pause(self):
[ef387cf]310        """Non-blocking pause.  Job must be playing (see
311        'GutenbachJob.is_playing').
[951ab1b]312
313        Raises
314        ------
315        InvalidJobStateException
316            If the job is not playing.
[33ea505]317
318        """
319       
320        if not self.is_playing:
[951ab1b]321            raise errors.InvalidJobStateException(self.state)
[33ea505]322        self.player.mplayer_pause()
[34a4e5d]323
324    def cancel(self):
[b8c3505]325        """Blocking cancel. The job must not have been previously
[ef387cf]326        aborted or completed (though this method will succeed if it
327        was previously cancelled).  This should be used to stop the
328        job following an external request.
[951ab1b]329
330        Raises
331        ------
332        InvalidJobStateException
333            If the job has already finished.
334
335        """
336       
[34a4e5d]337        if self.is_playing:
[b8c3505]338            self.player._callback = None
[34a4e5d]339            self.player.mplayer_stop()
[b8c3505]340
[345c476]341        elif self.is_done and not self._why_done == "cancelled":
[951ab1b]342            raise errors.InvalidJobStateException(self.state)
[b8c3505]343
344        logger.info("cancelled job %s" % str(self))
345        self._why_done = "cancelled"
[eee389a]346
[34a4e5d]347    def abort(self):
[b8c3505]348        """Blocking abort. The job must not have been previously
[ef387cf]349        cancelled or completed (though this method will succeed if it
350        was previously aborted).  This should be used to stop the job
351        following internal errors.
[951ab1b]352
353        Raises
354        ------
355        InvalidJobStateException
356            If the job has already finished.
357
358        """
359
[34a4e5d]360        if self.is_playing:
[b8c3505]361            self.player._callback = None
[eee389a]362            self.player.mplayer_stop()
[b8c3505]363
[345c476]364        elif self.is_done and not self._why_done == "aborted":
[951ab1b]365            raise errors.InvalidJobStateException(self.state)
[b8c3505]366
367        logger.info("aborted job %s" % str(self))
368        self._why_done = "aborted"
[776a659]369
[609a9b0]370    def restart(self):
371        # XXX: Todo
372        pass
Note: See TracBrowser for help on using the repository browser.