# SPDX-FileCopyrightText: 2022 Michael Pyne # # SPDX-License-Identifier: GPL-2.0-or-later package ksb::Util::LoggedSubprocess 0.10; use ksb; =head1 SYNOPSIS my $cmd = ksb::Util::LoggedSubprocess->new ->module($module) # required ->log_to($filename) # required ->set_command($argRef) # required ->chdir_to($builddir) # optional ->announcer(sub ($mod) { # optional note("g[$mod] starting update") }) ; # optional, can have child output forwarded back to parent for processing $cmd->on(child_output => sub ($cmd, $line) { # called in parent! $log_command_callback->($line); }); # once ready, call ->start to obtain a Mojo::Promise that # can be waited on or chained from, pending the result of # computation in a separate child process. my $promise = $cmd->start->then(sub ($exitcode) { $resultRef = { was_successful => $exitcode == 0, warnings => $warnings, work_done => $workDoneFlag, }; }); =cut =head1 DESCRIPTION This is a subclass of L which integrates the functionality of that class into kdesrc-build's logging and module tracking functions. Like Mojolicious (and unlike most of the rest of kdesrc-build), this is a 'fluent' interface due to the number of adjustables vars that must be set, including which module is being built, the log file to use, what directory to build from, etc. =cut use Mojo::Base 'Mojo::EventEmitter'; use Mojo::Promise; use Mojo::IOLoop::Subprocess; use ksb::BuildException qw(croak_internal); use ksb::Debug; use ksb::Util qw(assert_isa disable_locale_message_translation p_chdir run_logged_command); use experimental 'signatures'; =head1 EVENTS =head2 child_output This event (see L, which is a base class of this one) is called whenever a line of output is produced in the child. Use the base class's C method to subscribe to the event. Any subscriptions to this event must be in place before C is called, as the child will not install a callback for this unless at least one subscriber is in place. =cut =head1 ATTRIBUTES These attributes are the configurable options that should be set before calling C to execute the desired command. If called without arguments, returns the existing value. See L for more information on how attributes work. =head2 module Sets the L that is being executed against. =head2 log_to Sets the base filename (without a .log extension) that should receive command output in the log directory. This must be set even if child output will not be examined. =head2 chdir_to Sets the directory to run the command from just before execution in the child process. Optional, if not set the directory will not be changed. The directory is never changed for the parent process! =head2 set_command Sets the command, and any arguments, to be run, as a reference to a list. E.g. $cmd->set_command(['make', '-j4']); =head2 disable_translations Optional. If set to a true value, causes the child process to attempt to disable command localization by setting the "C" locale in the shell environment. This can be needed for filtering command output but should be avoided if possible otherwise. =head2 announcer Optional. Can be set to a sub that will be called with a single parameter (the ksb::Module being built) in the child process just before the build starts. You can use this to make an announcement just before the command is run since there's no way to guarantee the timing in a longer build. =cut has 'module'; has 'log_to'; has 'chdir_to'; has 'set_command'; has 'disable_translations' => 0; has 'announcer'; =head1 METHODS =cut =head2 start Begins the execution, if possible. Returns a L that resolves to the exit code of the command being run. 0 indicates success, non-zero indicates failure. Exceptions may be thrown, which L will catch and convert into a rejected promise. You must install a L handler on the promise to handle this condition. =cut sub start($self) { assert_isa(my $module = $self->module, 'ksb::Module'); croak_internal('Need to log somewhere') unless my $filename = $self->log_to; croak_internal('No command to run!') unless my $argRef = $self->set_command; croak_internal('Command list needs to be a listref!') unless ref $argRef eq 'ARRAY'; my $dir_to_run_from = $self->chdir_to; my $announceSub = $self->announcer; my @command = @{$argRef}; if (pretending()) { local $" = "]', 'g["; pretend ("\tWould have run ('g[@command]')"); return Mojo::Promise->resolve(0); } my $subp = Mojo::IOLoop::Subprocess->new; # Install callback handler to feed child output to parent if the parent has # a callback to filter through it. my $needsCallback = $self->has_subscribers('child_output'); if ($needsCallback) { $subp->on(progress => sub ($cmd, $data) { my $line = $data->{child_data} // undef; if ($line) { $self->emit(child_output => $line->[0]); return; } die "unimplemented " . join(', ', keys %$data) if ref $data eq 'HASH'; die "unimplemented $data"; }); } my $succeeded = 0; return $subp->run_p(sub { # in a child process p_chdir($dir_to_run_from) if $dir_to_run_from; disable_locale_message_translation() if $self->disable_translations(); my $callback; if ($needsCallback) { $callback = sub ($line) { return unless defined $line; $self->_sendToParent($subp, $line); } } $announceSub->($module) if $announceSub; my $result = run_logged_command($module, $filename, $callback, @command); whisper("$command[0] complete, result $result"); return $result; })->then(sub ($exitcode) { $succeeded = ($exitcode == 0); return $exitcode; # Don't change result, just pass it on })->finally(sub { # If an exception was thrown or we didn't succeed, set error log ksb::Util::_setErrorLogfile($module, "$filename.log") unless $succeeded; }); } # Sends the given data to the parent process. Our calling code and this # package must share the same single channel (over the 'progress' event # supported by Mojolicious). Although we only support handling for the calling # code (to send line-by-line output back to the parent), to support future # expansion we send a hashref which we can add different keys to if we need to # support other use cases. sub _sendToParent($self, $cmd, @data) { $cmd->progress({ child_data => [@data], }); } 1;