# SPDX-FileCopyrightText: 2022 Michael Pyne # # SPDX-License-Identifier: GPL-2.0-or-later package ksb::TaskManager 0.10; use ksb; =head1 SYNOPSIS assert_isa($app, 'ksb::Application'); my $mgr = ksb::TaskManager->new($app); # build context must be setup first my $result = eval { $mgr->runAllTasks(); } # all module updates/builds/etc. complete =head1 DESCRIPTION This module consolidates the actual orchestration of all the module update, buildsystem setup, configure, build, and install jobs once the L has setup the L for the current build. In particular, the concurrent portion of the build is concentrated more-or-less entirely within "runAllTasks", although other parts of the script have to be aware of concurrency. =cut use ksb::Debug qw(:DEFAULT colorize); use ksb::IPC::Pipe; use ksb::IPC::Null; use ksb::Util; use Mojo::URL; use IO::Select; use POSIX qw(EINTR WNOHANG); my $DO_STOP = 0; sub new ($class, $app) { assert_isa($app, 'ksb::Application'); my $opts = { ksb_app => $app, }; return bless $opts, $class; } # returns shell-style result code sub runAllTasks ($self) { # What we're going to do is fork another child to perform the source # updates while we build. Setup for this first by initializing some # shared memory. my $ctx = $self->{ksb_app}->context(); my $result = 0; my $ipc; my $updateOptsSub = sub ($modName, $k, $v) { $ctx->setPersistentOption($modName, $k, $v); }; if ($ctx->usesConcurrentPhases() && $ctx->getOption('async')) { $ipc = ksb::IPC::Pipe->new() } else { $ipc = ksb::IPC::Null->new(); } $ipc->setPersistentOptionHandler($updateOptsSub); if ($ipc->supportsConcurrency()) { $result = _handle_async_build ($ipc, $ctx); $ipc->outputPendingLoggedMessages() if debugging(); } else { whisper ("Using no IPC mechanism\n"); # If the user sends SIGHUP during the build, we should allow the # current module to complete and then exit early. local $SIG{HUP} = sub { say "[noasync] recv SIGHUP, will end after this module"; $DO_STOP = 1; }; note ("\n b[<<< Update Process >>>]\n"); $result = _handle_updates ($ipc, $ctx); note (" b[<<< Build Process >>>]\n"); $result = _handle_build ($ipc, $ctx) || $result; } return $result; } # Internal API # Function: _handle_updates # # Subroutine to update a list of modules. # # Parameters: # 1. IPC module to pass results to. # 2. Build Context, which will be used to determine the module update list. # # The ipc parameter contains an object that is responsible for communicating # the status of building the modules. This function must account for every # module in $ctx's update phase to the ipc object before returning. # # Returns 0 on success, non-zero on error. sub _handle_updates ($ipc, $ctx) { my @update_list = $ctx->modulesInPhase('update'); # No reason to print out the text if we're not doing anything. if (!@update_list) { $ipc->sendIPCMessage(ksb::IPC::ALL_UPDATING, "update-list-empty"); $ipc->sendIPCMessage(ksb::IPC::ALL_DONE, "update-list-empty"); return 0; } if (not _check_for_ssh_agent($ctx)) { $ipc->sendIPCMessage(ksb::IPC::ALL_FAILURE, "ssh-failure"); return 1; } my $kdesrc = $ctx->getSourceDir(); if (not -e $kdesrc) { whisper ("KDE source download directory doesn't exist, creating.\n"); if (not super_mkdir ($kdesrc)) { error ("Unable to make directory r[$kdesrc]!"); $ipc->sendIPCMessage(ksb::IPC::ALL_FAILURE, "no-source-dir"); return 1; } } # Once at this point, any errors we get should be limited to a module, # which means we can tell the build thread to start. $ipc->sendIPCMessage(ksb::IPC::ALL_UPDATING, "starting-updates"); my $hadError = 0; foreach my $module (@update_list) { if ($DO_STOP) { note (" y[b[* * *] Early exit requested, aborting updates."); last; } $ipc->setLoggedModule($module->name()); # Note that this must be in this order to avoid accidentally not # running ->update() from short-circuiting if an error is noted. $hadError = !$module->update($ipc, $ctx) || $hadError; # Cache module directories, e.g. to be consumed in kdesrc-build --run # This is needed for --no-async mode where the buildSingleModule won't run # But the other one is needed for --async mode since persistent options # only work from within the build process $module->setPersistentOption('source-dir', $module->fullpath('source')); } $ipc->sendIPCMessage(ksb::IPC::ALL_DONE, "had_errors: $hadError"); return $hadError; } # Builds the given module. # # Return value is the failure phase, or 0 on success. sub _buildSingleModule ($ipc, $ctx, $module, $startTimeRef) { $ctx->resetEnvironment(); $module->setupEnvironment(); # Cache module directories, e.g. to be consumed in kdesrc-build --run $module->setPersistentOption('source-dir', $module->fullpath('source')); $module->setPersistentOption('build-dir', $module->fullpath('build')); $module->setPersistentOption('install-dir', $module->installationPath()); my $fail_count = $module->getPersistentOption('failure-count') // 0; my ($resultStatus, $message) = $ipc->waitForModule($module); $ipc->forgetModule($module); if ($resultStatus eq 'failed') { error ("\tUnable to update r[$module], build canceled."); $module->setPersistentOption('failure-count', ++$fail_count); return 'update'; } elsif ($resultStatus eq 'success') { note ("\tSource update complete for g[$module]: $message"); my $whyRefresh = $ipc->refreshReasonFor($module->name()); info ("\t Rebuilding because $whyRefresh") if $whyRefresh; } # Skip actually building a module if the user has selected to skip # builds when the source code was not actually updated. But, don't skip # if we didn't successfully build last time. elsif ($resultStatus eq 'skipped' && !$module->getOption('build-when-unchanged') && $fail_count == 0) { note ("\tSkipping g[$module] because its source code has not changed."); return 0; } elsif ($resultStatus eq 'skipped') { note ("\tNo changes to g[$module] source code, but proceeding to build anyway."); } # If the build gets interrupted, ensure the persistent options that are # written reflect that the build failed by preemptively setting the future # value to write. If the build succeeds we'll reset to 0 then. $module->setPersistentOption('failure-count', $fail_count + 1); $$startTimeRef = time; if ($module->build()) { $module->setPersistentOption('failure-count', 0); return 0; } return 'build'; # phase failed at } # Function: _handle_build # # Subroutine to handle the build process. # # Parameters: # 1. IPC object to receive results from. # 2. Build Context, which is used to determine list of modules to build. # # If the packages are not already checked-out and/or updated, this # subroutine WILL NOT do so for you. # # This subroutine assumes that the source directory has already been set up. # It will create the build directory if it doesn't already exist. # # If $builddir/$module/.refresh-me exists, the subroutine will # completely rebuild the module (as if --refresh-build were passed for that # module). # # Returns 0 for success, non-zero for failure. sub _handle_build ($ipc, $ctx) { my @modules = $ctx->modulesInPhase('build'); # No reason to print building messages if we're not building. return 0 unless @modules; # IPC queue should have a message saying whether or not to bother with the # build. $ipc->waitForStreamStart(); $ctx->unsetPersistentOption('global', 'resume-list'); my $outfile = pretending() ? '/dev/null' : $ctx->getLogDir() . '/build-status'; open (my $status_fh, '>', $outfile) or do { error (<statusViewer(); $statusViewer->numberModulesTotal($num_modules); while (my $module = shift @modules) { if ($DO_STOP) { note (" y[b[* * *] Early exit requested, aborting updates."); last; } my $moduleName = $module->name(); my $moduleSet = $module->moduleSet()->name(); my $modOutput = $moduleName; if (debugging(ksb::Debug::WHISPER)) { my $sysType = $module->buildSystemType(); $modOutput .= " (build system $sysType)"; } $moduleSet = " from g[$moduleSet]" if $moduleSet; note ("Building g[$modOutput]$moduleSet ($cur_module/$num_modules)"); my $start_time = time; my $failedPhase = _buildSingleModule($ipc, $ctx, $module, \$start_time); my $elapsed = prettify_seconds(time - $start_time); if ($failedPhase) { # FAILURE $ctx->markModulePhaseFailed($failedPhase, $module); say $status_fh "$module: Failed on $failedPhase after $elapsed."; if ($result == 0) { # No failures yet, mark this as resume point my $moduleList = join(', ', map { "$_" } ($module, @modules)); $ctx->setPersistentOption('global', 'resume-list', $moduleList); } $result = 1; if ($module->getOption('stop-on-failure')) { note ("\n$module didn't build, stopping here."); return 1; # Error } $statusViewer->numberModulesFailed(1 + $statusViewer->numberModulesFailed); } else { # Success say $status_fh "$module: Succeeded after $elapsed."; push @build_done, $moduleName; # Make it show up as a success $statusViewer->numberModulesSucceeded(1 + $statusViewer->numberModulesSucceeded); } $cur_module++; print "\n"; # Space things out } if ($outfile) { close $status_fh; # Update the symlink in latest to point to this file. my $logdir = $ctx->getSubdirPath('log-dir'); my $statusFileLoc = "$logdir/latest/build-status"; safe_unlink($statusFileLoc) if -l $statusFileLoc; symlink($outfile, $statusFileLoc); } info ("<<< g[PACKAGES SUCCESSFULLY BUILT] >>>") if scalar @build_done > 0; my $successes = scalar @build_done; my $mods = $successes == 1 ? 'module' : 'modules'; if (not pretending()) { # Print out results, and output to a file my $kdesrc = $ctx->getSourceDir(); open (my $built, '>', "$kdesrc/successfully-built"); foreach my $module (@build_done) { info ("$module") if $successes <= 10; say $built "$module"; } close $built; info ("Built g[$successes] $mods") if $successes > 10; } else { # Just print out the results if ($successes <= 10) { info ('g[', join ("]\ng[", @build_done), ']'); } else { info ("Built g[$successes] $mods") if $successes > 10; } } return $result; } # Function: _handle_async_build # # This subroutine special-cases the handling of the update and build phases, by # performing them concurrently (where possible), using forked processes. # # Only one thread or process of execution will return from this procedure. Any # other processes will be forced to exit after running their assigned module # phase(s). # # We also redirect ksb::Debug output messages to be sent to a single process # for display on the terminal instead of allowing them all to interrupt each # other. # # Parameters: # 1. IPC Object to use for sending/receiving update/build status. It must be # an object type that supports IPC concurrency (e.g. IPC::Pipe). # 2. Build Context to use, from which the module lists will be determined. # # Returns 0 on success, non-zero on failure. sub _handle_async_build ($ipc, $ctx) { # The exact method for async is that two children are forked. One child # is a source update process. The other child is a monitor process which will # hold status updates from the update process so that the updates may # happen without waiting for us to be ready to read. print "\n"; # Space out from metadata messages. # Before we fork we should pre-calculate where the logs will go so that the # children do not try to do the same calculation independently because they # didn't know it's already been figured out. $_->getLogDir() foreach @{$ctx->moduleList()}; my $result = 0; my $monitorPid = fork; my $updaterPid; if ($monitorPid == 0) { # child my $updaterToMonitorIPC = ksb::IPC::Pipe->new(); $updaterPid = fork; $SIG{INT} = sub { POSIX::_exit(EINTR); }; if ($updaterPid == 0) { # child of monitor # If the user sends SIGHUP during the build, we should allow the # current module to complete and then exit early. local $SIG{HUP} = sub { say "[updater] recv SIGHUP, will end after this module"; $DO_STOP = 1; }; $0 = 'kdesrc-build-updater'; $updaterToMonitorIPC->setSender(); ksb::Debug::setIPC($updaterToMonitorIPC); POSIX::_exit (_handle_updates ($updaterToMonitorIPC, $ctx)); } else { # still monitor # If the user sends SIGHUP during the build, we should allow the # current module to complete and then exit early. local $SIG{HUP} = sub { say "[monitor] recv SIGHUP, will end after this module"; # If we haven't recv'd yet, forward to monitor in case user didn't # send to process group kill 'HUP', $updaterPid unless $DO_STOP; $DO_STOP = 1; }; $0 = 'kdesrc-build-monitor'; $ipc->setSender(); $updaterToMonitorIPC->setReceiver(); $ipc->setLoggedModule('#monitor#'); # This /should/ never be used... ksb::Debug::setIPC($ipc); my $exitcode = _handle_monitoring ($ipc, $updaterToMonitorIPC); if (waitpid ($updaterPid, WNOHANG) == 0) { error (" r[b[***] updater thread is finished but hasn't exited?!?"); } POSIX::_exit ($exitcode); } } else { # Still the parent, let's do the build. # If the user sends SIGHUP during the build, we should allow the current # module to complete and then exit early. local $SIG{HUP} = sub { say "[ build ] recv SIGHUP, will end after this module"; # If we haven't recv'd yet, forward to monitor in case user didn't # send to process group kill 'HUP', $monitorPid unless $DO_STOP; $DO_STOP = 1; }; $0 = 'kdesrc-build-build'; $ipc->setReceiver(); $result = _handle_build ($ipc, $ctx); } $ipc->waitForEnd(); $ipc->close(); # Display a message for updated modules not listed because they were not # built. my $unseenModulesRef = $ipc->unacknowledgedModules(); if (%$unseenModulesRef) { # The only current way we should get unacknowledged modules is if the # build thread manages to end earlier than the update thread. This # should only happen under --stop-on-failure if an early build fails. # # If an update fails the message will still be printed to the user, so # we don't need to note it separately here, and there's no need to list # one-by-one the modules that successfully updated. whisper ("Some modules were updated but not built"); } # It's possible if build fails on first module that git is still # running. Make it stop too. if (waitpid ($monitorPid, WNOHANG) == 0) { kill 'INT', $monitorPid; # Exit code is in $?. waitpid ($monitorPid, 0); $result = 1 if $? != 0; } return $result; } # Function: _check_for_ssh_agent # # Checks if we are supposed to use ssh agent by examining the environment, and # if so checks if ssh-agent has a list of identities. If it doesn't, we run # ssh-add (with no arguments) and inform the user. This can be controlled with # the disable-agent-check parameter. # # Parameters: # 1. Build context sub _check_for_ssh_agent ($ctx) { # Don't bother with all this if the user isn't even using SSH. return 1 if pretending(); return 1 if $ctx->getOption('disable-agent-check'); my @gitServers = grep { $_->scmType() eq 'git' } ($ctx->modulesInPhase('update')); my @sshServers, grep { # Check for git+ssh:// or git@git.kde.org:/path/etc. my $url = Mojo::URL->new($_->getOption('repository')); ($url->scheme eq 'git+ssh') || (($url->userinfo // '') eq 'git' && $url->host eq 'git.kde.org'); } @gitServers; return 1 unless @sshServers; whisper ("\tChecking for SSH Agent"); # We're using ssh to download, see if ssh-agent is running. return 1 unless exists $ENV{'SSH_AGENT_PID'}; my $pid = $ENV{'SSH_AGENT_PID'}; # It's supposed to be running, let's see if there exists the program with # that pid (this check is linux-specific at the moment). if (-d "/proc" and not -e "/proc/$pid") { local $" = ', '; # override list interpolation separator warning (<getOption('ssh-identity-file'); push (@commandLine, $identFile) if $identFile; my $result = system (@commandLine); # Run this code for both death-by-signal and nonzero return if ($result) { my $rcfile = $ctx->rcFile(); say colorize(<{fh} || croak_runtime('??? missing pipe to build proc'); my $recvFH = $ipcFromUpdater->{fh} || croak_runtime('??? missing pipe from monitor'); my $readSelector = IO::Select->new($recvFH); my $writeSelector = IO::Select->new($sendFH); # Start the loop. We will be waiting on either read or write ends. # Whenever select() returns we must check both sets. while ( my ($readReadyRef, $writeReadyRef) = IO::Select->select($readSelector, $writeSelector, undef)) { if (!$readReadyRef && !$writeReadyRef) { # Some kind of error occurred. return 1; } # Check for source updates first. if (@{$readReadyRef}) { undef $@; my $msg = eval { $ipcFromUpdater->receiveMessage(); }; # undef msg indicates EOF, so check for exception obj specifically die $@ if $@; # undef can be returned on EOF as well as error. EOF means the # other side is presumably done. if (! defined $msg) { $readSelector->remove($recvFH); last; # Select no longer needed, just output to build. } else { push @msgs, $msg; # We may not have been waiting for write handle to be ready if # we were blocking on an update from updater thread. $writeSelector->add($sendFH) unless $writeSelector->exists($sendFH); } } # Now check for build updates. if (@{$writeReadyRef}) { # If we're here the update is still going. If we have no messages # to send wait for that first. if (not @msgs) { $writeSelector->remove($sendFH); } else { # Send the message (if we got one). if (!$ipcToBuild->sendMessage(shift @msgs)) { error ("r[mon]: Build process stopped too soon! r[$!]"); return 1; } } } } # Send all remaining messages. for my $msg (@msgs) { if (!$ipcToBuild->sendMessage($msg)) { error ("r[mon]: Build process stopped too soon! r[$!]"); return 1; } } $ipcToBuild->close(); return 0; } 1;