1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun 5*4882a593Smuzhiyunfrom django.core.management.base import BaseCommand 6*4882a593Smuzhiyunfrom django.db import transaction 7*4882a593Smuzhiyunfrom django.db.models import Q 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunfrom bldcontrol.bbcontroller import getBuildEnvironmentController 10*4882a593Smuzhiyunfrom bldcontrol.models import BuildRequest, BuildEnvironment 11*4882a593Smuzhiyunfrom bldcontrol.models import BRError, BRVariable 12*4882a593Smuzhiyun 13*4882a593Smuzhiyunfrom orm.models import Build, LogMessage, Target 14*4882a593Smuzhiyun 15*4882a593Smuzhiyunimport logging 16*4882a593Smuzhiyunimport traceback 17*4882a593Smuzhiyunimport signal 18*4882a593Smuzhiyunimport os 19*4882a593Smuzhiyun 20*4882a593Smuzhiyunlogger = logging.getLogger("toaster") 21*4882a593Smuzhiyun 22*4882a593Smuzhiyun 23*4882a593Smuzhiyunclass Command(BaseCommand): 24*4882a593Smuzhiyun args = "" 25*4882a593Smuzhiyun help = "Schedules and executes build requests as possible. "\ 26*4882a593Smuzhiyun "Does not return (interrupt with Ctrl-C)" 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun @transaction.atomic 29*4882a593Smuzhiyun def _selectBuildEnvironment(self): 30*4882a593Smuzhiyun bec = getBuildEnvironmentController(lock=BuildEnvironment.LOCK_FREE) 31*4882a593Smuzhiyun bec.be.lock = BuildEnvironment.LOCK_LOCK 32*4882a593Smuzhiyun bec.be.save() 33*4882a593Smuzhiyun return bec 34*4882a593Smuzhiyun 35*4882a593Smuzhiyun @transaction.atomic 36*4882a593Smuzhiyun def _selectBuildRequest(self): 37*4882a593Smuzhiyun br = BuildRequest.objects.filter(state=BuildRequest.REQ_QUEUED).first() 38*4882a593Smuzhiyun return br 39*4882a593Smuzhiyun 40*4882a593Smuzhiyun def schedule(self): 41*4882a593Smuzhiyun try: 42*4882a593Smuzhiyun # select the build environment and the request to build 43*4882a593Smuzhiyun br = self._selectBuildRequest() 44*4882a593Smuzhiyun if br: 45*4882a593Smuzhiyun br.state = BuildRequest.REQ_INPROGRESS 46*4882a593Smuzhiyun br.save() 47*4882a593Smuzhiyun else: 48*4882a593Smuzhiyun return 49*4882a593Smuzhiyun 50*4882a593Smuzhiyun try: 51*4882a593Smuzhiyun bec = self._selectBuildEnvironment() 52*4882a593Smuzhiyun except IndexError as e: 53*4882a593Smuzhiyun # we could not find a BEC; postpone the BR 54*4882a593Smuzhiyun br.state = BuildRequest.REQ_QUEUED 55*4882a593Smuzhiyun br.save() 56*4882a593Smuzhiyun logger.debug("runbuilds: No build env (%s)" % e) 57*4882a593Smuzhiyun return 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun logger.info("runbuilds: starting build %s, environment %s" % 60*4882a593Smuzhiyun (br, bec.be)) 61*4882a593Smuzhiyun 62*4882a593Smuzhiyun # let the build request know where it is being executed 63*4882a593Smuzhiyun br.environment = bec.be 64*4882a593Smuzhiyun br.save() 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun # this triggers an async build 67*4882a593Smuzhiyun bec.triggerBuild(br.brbitbake, br.brlayer_set.all(), 68*4882a593Smuzhiyun br.brvariable_set.all(), br.brtarget_set.all(), 69*4882a593Smuzhiyun "%d:%d" % (br.pk, bec.be.pk)) 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun except Exception as e: 72*4882a593Smuzhiyun logger.error("runbuilds: Error launching build %s" % e) 73*4882a593Smuzhiyun traceback.print_exc() 74*4882a593Smuzhiyun if "[Errno 111] Connection refused" in str(e): 75*4882a593Smuzhiyun # Connection refused, read toaster_server.out 76*4882a593Smuzhiyun errmsg = bec.readServerLogFile() 77*4882a593Smuzhiyun else: 78*4882a593Smuzhiyun errmsg = str(e) 79*4882a593Smuzhiyun 80*4882a593Smuzhiyun BRError.objects.create(req=br, errtype=str(type(e)), errmsg=errmsg, 81*4882a593Smuzhiyun traceback=traceback.format_exc()) 82*4882a593Smuzhiyun br.state = BuildRequest.REQ_FAILED 83*4882a593Smuzhiyun br.save() 84*4882a593Smuzhiyun bec.be.lock = BuildEnvironment.LOCK_FREE 85*4882a593Smuzhiyun bec.be.save() 86*4882a593Smuzhiyun # Cancel the pending build and report the exception to the UI 87*4882a593Smuzhiyun log_object = LogMessage.objects.create( 88*4882a593Smuzhiyun build = br.build, 89*4882a593Smuzhiyun level = LogMessage.EXCEPTION, 90*4882a593Smuzhiyun message = errmsg) 91*4882a593Smuzhiyun log_object.save() 92*4882a593Smuzhiyun br.build.outcome = Build.FAILED 93*4882a593Smuzhiyun br.build.save() 94*4882a593Smuzhiyun 95*4882a593Smuzhiyun def archive(self): 96*4882a593Smuzhiyun for br in BuildRequest.objects.filter(state=BuildRequest.REQ_ARCHIVE): 97*4882a593Smuzhiyun if br.build is None: 98*4882a593Smuzhiyun br.state = BuildRequest.REQ_FAILED 99*4882a593Smuzhiyun else: 100*4882a593Smuzhiyun br.state = BuildRequest.REQ_COMPLETED 101*4882a593Smuzhiyun br.save() 102*4882a593Smuzhiyun 103*4882a593Smuzhiyun def cleanup(self): 104*4882a593Smuzhiyun from django.utils import timezone 105*4882a593Smuzhiyun from datetime import timedelta 106*4882a593Smuzhiyun # environments locked for more than 30 seconds 107*4882a593Smuzhiyun # they should be unlocked 108*4882a593Smuzhiyun BuildEnvironment.objects.filter( 109*4882a593Smuzhiyun Q(buildrequest__state__in=[BuildRequest.REQ_FAILED, 110*4882a593Smuzhiyun BuildRequest.REQ_COMPLETED, 111*4882a593Smuzhiyun BuildRequest.REQ_CANCELLING]) & 112*4882a593Smuzhiyun Q(lock=BuildEnvironment.LOCK_LOCK) & 113*4882a593Smuzhiyun Q(updated__lt=timezone.now() - timedelta(seconds=30)) 114*4882a593Smuzhiyun ).update(lock=BuildEnvironment.LOCK_FREE) 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun # update all Builds that were in progress and failed to start 117*4882a593Smuzhiyun for br in BuildRequest.objects.filter( 118*4882a593Smuzhiyun state=BuildRequest.REQ_FAILED, 119*4882a593Smuzhiyun build__outcome=Build.IN_PROGRESS): 120*4882a593Smuzhiyun # transpose the launch errors in ToasterExceptions 121*4882a593Smuzhiyun br.build.outcome = Build.FAILED 122*4882a593Smuzhiyun for brerror in br.brerror_set.all(): 123*4882a593Smuzhiyun logger.debug("Saving error %s" % brerror) 124*4882a593Smuzhiyun LogMessage.objects.create(build=br.build, 125*4882a593Smuzhiyun level=LogMessage.EXCEPTION, 126*4882a593Smuzhiyun message=brerror.errmsg) 127*4882a593Smuzhiyun br.build.save() 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun # we don't have a true build object here; hence, toasterui 130*4882a593Smuzhiyun # didn't have a change to release the BE lock 131*4882a593Smuzhiyun br.environment.lock = BuildEnvironment.LOCK_FREE 132*4882a593Smuzhiyun br.environment.save() 133*4882a593Smuzhiyun 134*4882a593Smuzhiyun # update all BuildRequests without a build created 135*4882a593Smuzhiyun for br in BuildRequest.objects.filter(build=None): 136*4882a593Smuzhiyun br.build = Build.objects.create(project=br.project, 137*4882a593Smuzhiyun completed_on=br.updated, 138*4882a593Smuzhiyun started_on=br.created) 139*4882a593Smuzhiyun br.build.outcome = Build.FAILED 140*4882a593Smuzhiyun try: 141*4882a593Smuzhiyun br.build.machine = br.brvariable_set.get(name='MACHINE').value 142*4882a593Smuzhiyun except BRVariable.DoesNotExist: 143*4882a593Smuzhiyun pass 144*4882a593Smuzhiyun br.save() 145*4882a593Smuzhiyun # transpose target information 146*4882a593Smuzhiyun for brtarget in br.brtarget_set.all(): 147*4882a593Smuzhiyun Target.objects.create(build=br.build, 148*4882a593Smuzhiyun target=brtarget.target, 149*4882a593Smuzhiyun task=brtarget.task) 150*4882a593Smuzhiyun # transpose the launch errors in ToasterExceptions 151*4882a593Smuzhiyun for brerror in br.brerror_set.all(): 152*4882a593Smuzhiyun LogMessage.objects.create(build=br.build, 153*4882a593Smuzhiyun level=LogMessage.EXCEPTION, 154*4882a593Smuzhiyun message=brerror.errmsg) 155*4882a593Smuzhiyun 156*4882a593Smuzhiyun br.build.save() 157*4882a593Smuzhiyun 158*4882a593Smuzhiyun # Make sure the LOCK is removed for builds which have been fully 159*4882a593Smuzhiyun # cancelled 160*4882a593Smuzhiyun for br in BuildRequest.objects.filter( 161*4882a593Smuzhiyun Q(build__outcome=Build.CANCELLED) & 162*4882a593Smuzhiyun Q(state=BuildRequest.REQ_CANCELLING) & 163*4882a593Smuzhiyun ~Q(environment=None)): 164*4882a593Smuzhiyun br.environment.lock = BuildEnvironment.LOCK_FREE 165*4882a593Smuzhiyun br.environment.save() 166*4882a593Smuzhiyun 167*4882a593Smuzhiyun def runbuild(self): 168*4882a593Smuzhiyun try: 169*4882a593Smuzhiyun self.cleanup() 170*4882a593Smuzhiyun except Exception as e: 171*4882a593Smuzhiyun logger.warning("runbuilds: cleanup exception %s" % str(e)) 172*4882a593Smuzhiyun 173*4882a593Smuzhiyun try: 174*4882a593Smuzhiyun self.archive() 175*4882a593Smuzhiyun except Exception as e: 176*4882a593Smuzhiyun logger.warning("runbuilds: archive exception %s" % str(e)) 177*4882a593Smuzhiyun 178*4882a593Smuzhiyun try: 179*4882a593Smuzhiyun self.schedule() 180*4882a593Smuzhiyun except Exception as e: 181*4882a593Smuzhiyun logger.warning("runbuilds: schedule exception %s" % str(e)) 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun # Test to see if a build pre-maturely died due to a bitbake crash 184*4882a593Smuzhiyun def check_dead_builds(self): 185*4882a593Smuzhiyun do_cleanup = False 186*4882a593Smuzhiyun try: 187*4882a593Smuzhiyun for br in BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS): 188*4882a593Smuzhiyun # Get the build directory 189*4882a593Smuzhiyun if br.project.builddir: 190*4882a593Smuzhiyun builddir = br.project.builddir 191*4882a593Smuzhiyun else: 192*4882a593Smuzhiyun builddir = '%s-toaster-%d' % (br.environment.builddir,br.project.id) 193*4882a593Smuzhiyun # Check log to see if there is a recent traceback 194*4882a593Smuzhiyun toaster_ui_log = os.path.join(builddir, 'toaster_ui.log') 195*4882a593Smuzhiyun test_file = os.path.join(builddir, '._toaster_check.txt') 196*4882a593Smuzhiyun os.system("tail -n 50 %s > %s" % (os.path.join(builddir, 'toaster_ui.log'),test_file)) 197*4882a593Smuzhiyun traceback_text = '' 198*4882a593Smuzhiyun is_traceback = False 199*4882a593Smuzhiyun with open(test_file,'r') as test_file_fd: 200*4882a593Smuzhiyun test_file_tail = test_file_fd.readlines() 201*4882a593Smuzhiyun for line in test_file_tail: 202*4882a593Smuzhiyun if line.startswith('Traceback (most recent call last):'): 203*4882a593Smuzhiyun traceback_text = line 204*4882a593Smuzhiyun is_traceback = True 205*4882a593Smuzhiyun elif line.startswith('NOTE: ToasterUI waiting for events'): 206*4882a593Smuzhiyun # Ignore any traceback before new build start 207*4882a593Smuzhiyun traceback_text = '' 208*4882a593Smuzhiyun is_traceback = False 209*4882a593Smuzhiyun elif line.startswith('Note: Toaster traceback auto-stop'): 210*4882a593Smuzhiyun # Ignore any traceback before this previous traceback catch 211*4882a593Smuzhiyun traceback_text = '' 212*4882a593Smuzhiyun is_traceback = False 213*4882a593Smuzhiyun elif is_traceback: 214*4882a593Smuzhiyun traceback_text += line 215*4882a593Smuzhiyun # Test the results 216*4882a593Smuzhiyun is_stop = False 217*4882a593Smuzhiyun if is_traceback: 218*4882a593Smuzhiyun # Found a traceback 219*4882a593Smuzhiyun errtype = 'Bitbake crash' 220*4882a593Smuzhiyun errmsg = 'Bitbake crash\n' + traceback_text 221*4882a593Smuzhiyun state = BuildRequest.REQ_FAILED 222*4882a593Smuzhiyun # Clean up bitbake files 223*4882a593Smuzhiyun bitbake_lock = os.path.join(builddir, 'bitbake.lock') 224*4882a593Smuzhiyun if os.path.isfile(bitbake_lock): 225*4882a593Smuzhiyun os.remove(bitbake_lock) 226*4882a593Smuzhiyun bitbake_sock = os.path.join(builddir, 'bitbake.sock') 227*4882a593Smuzhiyun if os.path.isfile(bitbake_sock): 228*4882a593Smuzhiyun os.remove(bitbake_sock) 229*4882a593Smuzhiyun if os.path.isfile(test_file): 230*4882a593Smuzhiyun os.remove(test_file) 231*4882a593Smuzhiyun # Add note to ignore this traceback on next check 232*4882a593Smuzhiyun os.system('echo "Note: Toaster traceback auto-stop" >> %s' % toaster_ui_log) 233*4882a593Smuzhiyun is_stop = True 234*4882a593Smuzhiyun # Add more tests here 235*4882a593Smuzhiyun #elif ... 236*4882a593Smuzhiyun # Stop the build request? 237*4882a593Smuzhiyun if is_stop: 238*4882a593Smuzhiyun brerror = BRError( 239*4882a593Smuzhiyun req = br, 240*4882a593Smuzhiyun errtype = errtype, 241*4882a593Smuzhiyun errmsg = errmsg, 242*4882a593Smuzhiyun traceback = traceback_text, 243*4882a593Smuzhiyun ) 244*4882a593Smuzhiyun brerror.save() 245*4882a593Smuzhiyun br.state = state 246*4882a593Smuzhiyun br.save() 247*4882a593Smuzhiyun do_cleanup = True 248*4882a593Smuzhiyun # Do cleanup 249*4882a593Smuzhiyun if do_cleanup: 250*4882a593Smuzhiyun self.cleanup() 251*4882a593Smuzhiyun except Exception as e: 252*4882a593Smuzhiyun logger.error("runbuilds: Error in check_dead_builds %s" % e) 253*4882a593Smuzhiyun 254*4882a593Smuzhiyun def handle(self, **options): 255*4882a593Smuzhiyun pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), 256*4882a593Smuzhiyun ".runbuilds.pid") 257*4882a593Smuzhiyun 258*4882a593Smuzhiyun with open(pidfile_path, 'w') as pidfile: 259*4882a593Smuzhiyun pidfile.write("%s" % os.getpid()) 260*4882a593Smuzhiyun 261*4882a593Smuzhiyun # Clean up any stale/failed builds from previous Toaster run 262*4882a593Smuzhiyun self.runbuild() 263*4882a593Smuzhiyun 264*4882a593Smuzhiyun signal.signal(signal.SIGUSR1, lambda sig, frame: None) 265*4882a593Smuzhiyun 266*4882a593Smuzhiyun while True: 267*4882a593Smuzhiyun sigset = signal.sigtimedwait([signal.SIGUSR1], 5) 268*4882a593Smuzhiyun if sigset: 269*4882a593Smuzhiyun for sig in sigset: 270*4882a593Smuzhiyun # Consume each captured pending event 271*4882a593Smuzhiyun self.runbuild() 272*4882a593Smuzhiyun else: 273*4882a593Smuzhiyun # Check for build exceptions 274*4882a593Smuzhiyun self.check_dead_builds() 275*4882a593Smuzhiyun 276