blob: 3390f637e3c518400c9c9adfd277460cff07970d [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#!/usr/bin/env python
2
3import os
4import sys
5import warnings
6sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(sys.argv[0])), 'lib'))
7from bb import fetch2
8import logging
9import bb
10import select
11import errno
12import signal
Patrick Williamsf1e5d692016-03-30 15:21:19 -050013from multiprocessing import Lock
Patrick Williamsc124f4f2015-09-15 14:41:29 -050014
15# Users shouldn't be running this code directly
16if len(sys.argv) != 2 or not sys.argv[1].startswith("decafbad"):
17 print("bitbake-worker is meant for internal execution by bitbake itself, please don't use it standalone.")
18 sys.exit(1)
19
20profiling = False
21if sys.argv[1] == "decafbadbad":
22 profiling = True
23 try:
24 import cProfile as profile
25 except:
26 import profile
27
28# Unbuffer stdout to avoid log truncation in the event
29# of an unorderly exit as well as to provide timely
30# updates to log files for use with tail
31try:
32 if sys.stdout.name == '<stdout>':
33 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
34except:
35 pass
36
37logger = logging.getLogger("BitBake")
38
39try:
40 import cPickle as pickle
41except ImportError:
42 import pickle
43 bb.msg.note(1, bb.msg.domain.Cache, "Importing cPickle failed. Falling back to a very slow implementation.")
44
45
46worker_pipe = sys.stdout.fileno()
47bb.utils.nonblockingfd(worker_pipe)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050048# Need to guard against multiprocessing being used in child processes
49# and multiple processes trying to write to the parent at the same time
50worker_pipe_lock = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -050051
52handler = bb.event.LogHandler()
53logger.addHandler(handler)
54
55if 0:
56 # Code to write out a log file of all events passing through the worker
57 logfilename = "/tmp/workerlogfile"
58 format_str = "%(levelname)s: %(message)s"
59 conlogformat = bb.msg.BBLogFormatter(format_str)
60 consolelog = logging.FileHandler(logfilename)
61 bb.msg.addDefaultlogFilter(consolelog)
62 consolelog.setFormatter(conlogformat)
63 logger.addHandler(consolelog)
64
65worker_queue = ""
66
67def worker_fire(event, d):
68 data = "<event>" + pickle.dumps(event) + "</event>"
69 worker_fire_prepickled(data)
70
71def worker_fire_prepickled(event):
72 global worker_queue
73
74 worker_queue = worker_queue + event
75 worker_flush()
76
77def worker_flush():
78 global worker_queue, worker_pipe
79
80 if not worker_queue:
81 return
82
83 try:
84 written = os.write(worker_pipe, worker_queue)
85 worker_queue = worker_queue[written:]
86 except (IOError, OSError) as e:
87 if e.errno != errno.EAGAIN and e.errno != errno.EPIPE:
88 raise
89
90def worker_child_fire(event, d):
91 global worker_pipe
Patrick Williamsf1e5d692016-03-30 15:21:19 -050092 global worker_pipe_lock
Patrick Williamsc124f4f2015-09-15 14:41:29 -050093
94 data = "<event>" + pickle.dumps(event) + "</event>"
95 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -050096 worker_pipe_lock.acquire()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050097 worker_pipe.write(data)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050098 worker_pipe_lock.release()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050099 except IOError:
100 sigterm_handler(None, None)
101 raise
102
103bb.event.worker_fire = worker_fire
104
105lf = None
106#lf = open("/tmp/workercommandlog", "w+")
107def workerlog_write(msg):
108 if lf:
109 lf.write(msg)
110 lf.flush()
111
112def sigterm_handler(signum, frame):
113 signal.signal(signal.SIGTERM, signal.SIG_DFL)
114 os.killpg(0, signal.SIGTERM)
115 sys.exit()
116
117def fork_off_task(cfg, data, workerdata, fn, task, taskname, appends, taskdepdata, quieterrors=False):
118 # We need to setup the environment BEFORE the fork, since
119 # a fork() or exec*() activates PSEUDO...
120
121 envbackup = {}
122 fakeenv = {}
123 umask = None
124
125 taskdep = workerdata["taskdeps"][fn]
126 if 'umask' in taskdep and taskname in taskdep['umask']:
127 # umask might come in as a number or text string..
128 try:
129 umask = int(taskdep['umask'][taskname],8)
130 except TypeError:
131 umask = taskdep['umask'][taskname]
132
133 # We can't use the fakeroot environment in a dry run as it possibly hasn't been built
134 if 'fakeroot' in taskdep and taskname in taskdep['fakeroot'] and not cfg.dry_run:
135 envvars = (workerdata["fakerootenv"][fn] or "").split()
136 for key, value in (var.split('=') for var in envvars):
137 envbackup[key] = os.environ.get(key)
138 os.environ[key] = value
139 fakeenv[key] = value
140
141 fakedirs = (workerdata["fakerootdirs"][fn] or "").split()
142 for p in fakedirs:
143 bb.utils.mkdirhier(p)
144 logger.debug(2, 'Running %s:%s under fakeroot, fakedirs: %s' %
145 (fn, taskname, ', '.join(fakedirs)))
146 else:
147 envvars = (workerdata["fakerootnoenv"][fn] or "").split()
148 for key, value in (var.split('=') for var in envvars):
149 envbackup[key] = os.environ.get(key)
150 os.environ[key] = value
151 fakeenv[key] = value
152
153 sys.stdout.flush()
154 sys.stderr.flush()
155
156 try:
157 pipein, pipeout = os.pipe()
158 pipein = os.fdopen(pipein, 'rb', 4096)
159 pipeout = os.fdopen(pipeout, 'wb', 0)
160 pid = os.fork()
161 except OSError as e:
162 bb.msg.fatal("RunQueue", "fork failed: %d (%s)" % (e.errno, e.strerror))
163
164 if pid == 0:
165 def child():
166 global worker_pipe
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500167 global worker_pipe_lock
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 pipein.close()
169
170 signal.signal(signal.SIGTERM, sigterm_handler)
171 # Let SIGHUP exit as SIGTERM
172 signal.signal(signal.SIGHUP, sigterm_handler)
173 bb.utils.signal_on_parent_exit("SIGTERM")
174
175 # Save out the PID so that the event can include it the
176 # events
177 bb.event.worker_pid = os.getpid()
178 bb.event.worker_fire = worker_child_fire
179 worker_pipe = pipeout
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500180 worker_pipe_lock = Lock()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181
182 # Make the child the process group leader and ensure no
183 # child process will be controlled by the current terminal
184 # This ensures signals sent to the controlling terminal like Ctrl+C
185 # don't stop the child processes.
186 os.setsid()
187 # No stdin
188 newsi = os.open(os.devnull, os.O_RDWR)
189 os.dup2(newsi, sys.stdin.fileno())
190
191 if umask:
192 os.umask(umask)
193
194 data.setVar("BB_WORKERCONTEXT", "1")
195 data.setVar("BB_TASKDEPDATA", taskdepdata)
196 data.setVar("BUILDNAME", workerdata["buildname"])
197 data.setVar("DATE", workerdata["date"])
198 data.setVar("TIME", workerdata["time"])
199 bb.parse.siggen.set_taskdata(workerdata["sigdata"])
200 ret = 0
201 try:
202 the_data = bb.cache.Cache.loadDataFull(fn, appends, data)
203 the_data.setVar('BB_TASKHASH', workerdata["runq_hash"][task])
204
205 # exported_vars() returns a generator which *cannot* be passed to os.environ.update()
206 # successfully. We also need to unset anything from the environment which shouldn't be there
207 exports = bb.data.exported_vars(the_data)
208 bb.utils.empty_environment()
209 for e, v in exports:
210 os.environ[e] = v
211 for e in fakeenv:
212 os.environ[e] = fakeenv[e]
213 the_data.setVar(e, fakeenv[e])
214 the_data.setVarFlag(e, 'export', "1")
215
216 if quieterrors:
217 the_data.setVarFlag(taskname, "quieterrors", "1")
218
219 except Exception as exc:
220 if not quieterrors:
221 logger.critical(str(exc))
222 os._exit(1)
223 try:
224 if cfg.dry_run:
225 return 0
226 return bb.build.exec_task(fn, taskname, the_data, cfg.profile)
227 except:
228 os._exit(1)
229 if not profiling:
230 os._exit(child())
231 else:
232 profname = "profile-%s.log" % (fn.replace("/", "-") + "-" + taskname)
233 prof = profile.Profile()
234 try:
235 ret = profile.Profile.runcall(prof, child)
236 finally:
237 prof.dump_stats(profname)
238 bb.utils.process_profilelog(profname)
239 os._exit(ret)
240 else:
241 for key, value in envbackup.iteritems():
242 if value is None:
243 del os.environ[key]
244 else:
245 os.environ[key] = value
246
247 return pid, pipein, pipeout
248
249class runQueueWorkerPipe():
250 """
251 Abstraction for a pipe between a worker thread and the worker server
252 """
253 def __init__(self, pipein, pipeout):
254 self.input = pipein
255 if pipeout:
256 pipeout.close()
257 bb.utils.nonblockingfd(self.input)
258 self.queue = ""
259
260 def read(self):
261 start = len(self.queue)
262 try:
263 self.queue = self.queue + self.input.read(102400)
264 except (OSError, IOError) as e:
265 if e.errno != errno.EAGAIN:
266 raise
267
268 end = len(self.queue)
269 index = self.queue.find("</event>")
270 while index != -1:
271 worker_fire_prepickled(self.queue[:index+8])
272 self.queue = self.queue[index+8:]
273 index = self.queue.find("</event>")
274 return (end > start)
275
276 def close(self):
277 while self.read():
278 continue
279 if len(self.queue) > 0:
280 print("Warning, worker child left partial message: %s" % self.queue)
281 self.input.close()
282
283normalexit = False
284
285class BitbakeWorker(object):
286 def __init__(self, din):
287 self.input = din
288 bb.utils.nonblockingfd(self.input)
289 self.queue = ""
290 self.cookercfg = None
291 self.databuilder = None
292 self.data = None
293 self.build_pids = {}
294 self.build_pipes = {}
295
296 signal.signal(signal.SIGTERM, self.sigterm_exception)
297 # Let SIGHUP exit as SIGTERM
298 signal.signal(signal.SIGHUP, self.sigterm_exception)
299
300 def sigterm_exception(self, signum, stackframe):
301 if signum == signal.SIGTERM:
302 bb.warn("Worker recieved SIGTERM, shutting down...")
303 elif signum == signal.SIGHUP:
304 bb.warn("Worker recieved SIGHUP, shutting down...")
305 self.handle_finishnow(None)
306 signal.signal(signal.SIGTERM, signal.SIG_DFL)
307 os.kill(os.getpid(), signal.SIGTERM)
308
309 def serve(self):
310 while True:
311 (ready, _, _) = select.select([self.input] + [i.input for i in self.build_pipes.values()], [] , [], 1)
312 if self.input in ready:
313 try:
314 r = self.input.read()
315 if len(r) == 0:
316 # EOF on pipe, server must have terminated
317 self.sigterm_exception(signal.SIGTERM, None)
318 self.queue = self.queue + r
319 except (OSError, IOError):
320 pass
321 if len(self.queue):
322 self.handle_item("cookerconfig", self.handle_cookercfg)
323 self.handle_item("workerdata", self.handle_workerdata)
324 self.handle_item("runtask", self.handle_runtask)
325 self.handle_item("finishnow", self.handle_finishnow)
326 self.handle_item("ping", self.handle_ping)
327 self.handle_item("quit", self.handle_quit)
328
329 for pipe in self.build_pipes:
330 self.build_pipes[pipe].read()
331 if len(self.build_pids):
332 self.process_waitpid()
333 worker_flush()
334
335
336 def handle_item(self, item, func):
337 if self.queue.startswith("<" + item + ">"):
338 index = self.queue.find("</" + item + ">")
339 while index != -1:
340 func(self.queue[(len(item) + 2):index])
341 self.queue = self.queue[(index + len(item) + 3):]
342 index = self.queue.find("</" + item + ">")
343
344 def handle_cookercfg(self, data):
345 self.cookercfg = pickle.loads(data)
346 self.databuilder = bb.cookerdata.CookerDataBuilder(self.cookercfg, worker=True)
347 self.databuilder.parseBaseConfiguration()
348 self.data = self.databuilder.data
349
350 def handle_workerdata(self, data):
351 self.workerdata = pickle.loads(data)
352 bb.msg.loggerDefaultDebugLevel = self.workerdata["logdefaultdebug"]
353 bb.msg.loggerDefaultVerbose = self.workerdata["logdefaultverbose"]
354 bb.msg.loggerVerboseLogs = self.workerdata["logdefaultverboselogs"]
355 bb.msg.loggerDefaultDomains = self.workerdata["logdefaultdomain"]
356 self.data.setVar("PRSERV_HOST", self.workerdata["prhost"])
357
358 def handle_ping(self, _):
359 workerlog_write("Handling ping\n")
360
361 logger.warn("Pong from bitbake-worker!")
362
363 def handle_quit(self, data):
364 workerlog_write("Handling quit\n")
365
366 global normalexit
367 normalexit = True
368 sys.exit(0)
369
370 def handle_runtask(self, data):
371 fn, task, taskname, quieterrors, appends, taskdepdata = pickle.loads(data)
372 workerlog_write("Handling runtask %s %s %s\n" % (task, fn, taskname))
373
374 pid, pipein, pipeout = fork_off_task(self.cookercfg, self.data, self.workerdata, fn, task, taskname, appends, taskdepdata, quieterrors)
375
376 self.build_pids[pid] = task
377 self.build_pipes[pid] = runQueueWorkerPipe(pipein, pipeout)
378
379 def process_waitpid(self):
380 """
381 Return none is there are no processes awaiting result collection, otherwise
382 collect the process exit codes and close the information pipe.
383 """
384 try:
385 pid, status = os.waitpid(-1, os.WNOHANG)
386 if pid == 0 or os.WIFSTOPPED(status):
387 return None
388 except OSError:
389 return None
390
391 workerlog_write("Exit code of %s for pid %s\n" % (status, pid))
392
393 if os.WIFEXITED(status):
394 status = os.WEXITSTATUS(status)
395 elif os.WIFSIGNALED(status):
396 # Per shell conventions for $?, when a process exits due to
397 # a signal, we return an exit code of 128 + SIGNUM
398 status = 128 + os.WTERMSIG(status)
399
400 task = self.build_pids[pid]
401 del self.build_pids[pid]
402
403 self.build_pipes[pid].close()
404 del self.build_pipes[pid]
405
406 worker_fire_prepickled("<exitcode>" + pickle.dumps((task, status)) + "</exitcode>")
407
408 def handle_finishnow(self, _):
409 if self.build_pids:
410 logger.info("Sending SIGTERM to remaining %s tasks", len(self.build_pids))
411 for k, v in self.build_pids.iteritems():
412 try:
413 os.kill(-k, signal.SIGTERM)
414 os.waitpid(-1, 0)
415 except:
416 pass
417 for pipe in self.build_pipes:
418 self.build_pipes[pipe].read()
419
420try:
421 worker = BitbakeWorker(sys.stdin)
422 if not profiling:
423 worker.serve()
424 else:
425 profname = "profile-worker.log"
426 prof = profile.Profile()
427 try:
428 profile.Profile.runcall(prof, worker.serve)
429 finally:
430 prof.dump_stats(profname)
431 bb.utils.process_profilelog(profname)
432except BaseException as e:
433 if not normalexit:
434 import traceback
435 sys.stderr.write(traceback.format_exc())
436 sys.stderr.write(str(e))
437while len(worker_queue):
438 worker_flush()
439workerlog_write("exitting")
440sys.exit(0)
441