76d856a535b6a17f7a32c4911711814e7466e397
[scons.git] / src / engine / SCons / Job.py
1 """SCons.Job
2
3 This module defines the Serial and Parallel classes that execute tasks to
4 complete a build. The Jobs class provides a higher level interface to start,
5 stop, and wait on jobs.
6
7 """
8
9 #
10 # Copyright (c) 2001, 2002, 2003 Steven Knight
11 #
12 # Permission is hereby granted, free of charge, to any person obtaining
13 # a copy of this software and associated documentation files (the
14 # "Software"), to deal in the Software without restriction, including
15 # without limitation the rights to use, copy, modify, merge, publish,
16 # distribute, sublicense, and/or sell copies of the Software, and to
17 # permit persons to whom the Software is furnished to do so, subject to
18 # the following conditions:
19 #
20 # The above copyright notice and this permission notice shall be included
21 # in all copies or substantial portions of the Software.
22 #
23 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
24 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
25 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 #
31
32 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
33
34 import time
35
36 class Jobs:
37     """An instance of this class initializes N jobs, and provides
38     methods for starting, stopping, and waiting on all N jobs.
39     """
40     
41     def __init__(self, num, taskmaster):
42         """
43         create 'num' jobs using the given taskmaster.
44
45         If 'num' is 1 or less, then a serial job will be used,
46         otherwise 'num' parallel jobs will be used.
47         """
48
49         # Keeps track of keyboard interrupts:
50         self.keyboard_interrupt = 0
51
52         if num > 1:
53             self.jobs = []
54             for i in range(num):
55                 self.jobs.append(Parallel(taskmaster, self))
56         else:
57             self.jobs = [Serial(taskmaster, self)]
58    
59         self.running = []
60
61     def run(self):
62         """run the jobs, and wait for them to finish"""
63
64         try:
65             for job in self.jobs:
66                 job.start()
67                 self.running.append(job)
68             while self.running:
69                 self.running[-1].wait()
70                 self.running.pop()
71         except KeyboardInterrupt:
72             # mask any further keyboard interrupts so that scons
73             # can shutdown cleanly:
74             # (this only masks the keyboard interrupt for Python,
75             #  child processes can still get the keyboard interrupt)
76             import signal
77             signal.signal(signal.SIGINT, signal.SIG_IGN)
78
79             for job in self.running:
80                 job.keyboard_interrupt()
81             else:
82                 self.keyboard_interrupt = 1
83
84             # wait on any remaining jobs to finish:
85             for job in self.running:
86                 job.wait()
87
88         if self.keyboard_interrupt:
89             raise KeyboardInterrupt
90     
91 class Serial:
92     """This class is used to execute tasks in series, and is more efficient
93     than Parallel, but is only appropriate for non-parallel builds. Only
94     one instance of this class should be in existence at a time.
95
96     This class is not thread safe.
97     """
98
99     def __init__(self, taskmaster, jobs):
100         """Create a new serial job given a taskmaster. 
101
102         The taskmaster's next_task() method should return the next task
103         that needs to be executed, or None if there are no more tasks. The
104         taskmaster's executed() method will be called for each task when it
105         is successfully executed or failed() will be called if it failed to
106         execute (e.g. execute() raised an exception). The taskmaster's
107         is_blocked() method will not be called.  """
108         
109         self.taskmaster = taskmaster
110         self.jobs = jobs
111
112     def start(self):
113         
114         """Start the job. This will begin pulling tasks from the taskmaster
115         and executing them, and return when there are no more tasks. If a task
116         fails to execute (i.e. execute() raises an exception), then the job will
117         stop."""
118         
119         while not self.jobs.keyboard_interrupt:
120             task = self.taskmaster.next_task()
121
122             if task is None:
123                 break
124
125             try:
126                 task.prepare()
127                 task.execute()
128             except KeyboardInterrupt:
129                 raise
130             except:
131                 # Let the failed() callback function arrange for the
132                 # build to stop if that's appropriate.
133                 task.failed()
134             else:
135                 task.executed()
136
137     def wait(self):
138         """Serial jobs are always finished when start() returns, so there
139         is nothing to do here"""
140         pass
141     
142     def keyboard_interrupt(self):
143         self.jobs.keyboard_interrupt = 1
144
145
146 # The will hold a condition variable once the first parallel task
147 # is created.
148 cv = None
149
150 class Parallel:
151     """This class is used to execute tasks in parallel, and is less
152     efficient than Serial, but is appropriate for parallel builds. Create
153     an instance of this class for each job or thread you want.
154
155     This class is thread safe.
156     """
157
158
159     def __init__(self, taskmaster, jobs):
160         """Create a new parallel job given a taskmaster, and a Jobs instance.
161         Multiple jobs will be using the taskmaster in parallel, but all
162         method calls to taskmaster methods are serialized by the jobs
163         themselves.
164
165         The taskmaster's next_task() method should return the next task
166         that needs to be executed, or None if there are no more tasks. The
167         taskmaster's executed() method will be called for each task when it
168         is successfully executed or failed() will be called if the task
169         failed to execute (i.e. execute() raised an exception).  The
170         taskmaster's is_blocked() method should return true iff there are
171         more tasks, but they can't be executed until one or more other
172         tasks have been executed. next_task() will be called iff
173         is_blocked() returned false.
174
175         Note: calls to taskmaster are serialized, but calls to execute() on
176         distinct tasks are not serialized, because that is the whole point
177         of parallel jobs: they can execute multiple tasks
178         simultaneously. """
179
180         global cv
181         
182         # import threading here so that everything in the Job module
183         # but the Parallel class will work if the interpreter doesn't
184         # support threads
185         import threading
186         
187         self.taskmaster = taskmaster
188         self.jobs = jobs
189         self.thread = threading.Thread(None, self.__run)
190
191         if cv is None:
192             cv = threading.Condition()
193
194     def start(self):
195         """Start the job. This will spawn a thread that will begin pulling
196         tasks from the task master and executing them. This method returns
197         immediately and doesn't wait for the jobs to be executed.
198
199         To wait for the job to finish, call wait().
200         """
201         self.thread.start()
202
203     def wait(self):
204         """Wait for the job to finish. A job is finished when there
205         are no more tasks.
206
207         This method should only be called after start() has been called.
208         """
209
210         # Sleeping in a loop like this is lame. Calling 
211         # self.thread.join() would be much nicer, but
212         # on Linux self.thread.join() doesn't always
213         # return when a KeyboardInterrupt happens, and when
214         # it does return, it causes Python to hang on shutdown.
215         # In other words this is just
216         # a work-around for some bugs/limitations in the
217         # self.thread.join() method.
218         while self.thread.isAlive():
219             time.sleep(0.5)
220
221     def keyboard_interrupt(self):
222         cv.acquire()
223         self.jobs.keyboard_interrupt = 1
224         cv.notifyAll()
225         cv.release()
226
227     def __run(self):
228         """private method that actually executes the tasks"""
229
230         cv.acquire()
231
232         try:
233
234             while 1:
235                 while (self.taskmaster.is_blocked() and 
236                        not self.jobs.keyboard_interrupt):
237                     cv.wait(None)
238
239                 if self.jobs.keyboard_interrupt:
240                     break
241                     
242                 task = self.taskmaster.next_task()
243
244                 if task == None:
245                     break
246
247                 try:
248                     task.prepare()
249                     cv.release()
250                     try:
251                         task.execute()
252                     finally:
253                         cv.acquire()
254                 except KeyboardInterrupt:
255                     self.jobs.keyboard_interrupt = 1
256                 except:
257                     # Let the failed() callback function arrange for
258                     # calling self.jobs.stop() to to stop the build
259                     # if that's appropriate.
260                     task.failed()
261                 else:
262                     task.executed()
263
264                 # signal the cv whether the task failed or not,
265                 # or otherwise the other Jobs might
266                 # remain blocked:
267                 if (not self.taskmaster.is_blocked() or
268                     self.jobs.keyboard_interrupt):
269                     cv.notifyAll()
270                     
271         finally:
272             cv.release()
273
274
275
276