diff --git a/src/lib/Hydra/Controller/Build.pm b/src/lib/Hydra/Controller/Build.pm index d05aef51..bde1030e 100644 --- a/src/lib/Hydra/Controller/Build.pm +++ b/src/lib/Hydra/Controller/Build.pm @@ -37,6 +37,7 @@ sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) { $c->stash->{project} = $c->stash->{build}->project; $c->stash->{jobset} = $c->stash->{build}->jobset; $c->stash->{job} = $c->stash->{build}->job; + $c->stash->{runcommandlogs} = [$c->stash->{build}->runcommandlogs->search({}, {order_by => ["id DESC"]})]; } diff --git a/src/lib/Hydra/Plugin/RunCommand.pm b/src/lib/Hydra/Plugin/RunCommand.pm index 38547fa2..1a122411 100644 --- a/src/lib/Hydra/Plugin/RunCommand.pm +++ b/src/lib/Hydra/Plugin/RunCommand.pm @@ -150,8 +150,20 @@ sub buildFinished { foreach my $commandToRun (@{$commandsToRun}) { my $command = $commandToRun->{command}; + + # todo: make all the to-run jobs "unstarted" in a batch, then start processing + my $runlog = $self->{db}->resultset("RunCommandLogs")->create({ + job_matcher => $commandToRun->{matcher}, + build_id => $build->get_column('id'), + command => $command + }); + + $runlog->started(); + system("$command") == 0 - or warn "notification command '$command' failed with exit status $?\n"; + or warn "notification command '$command' failed with exit status $? ($!)\n"; + + $runlog->completed_with_child_error($?, $!); } } diff --git a/src/lib/Hydra/Schema/Result/Builds.pm b/src/lib/Hydra/Schema/Result/Builds.pm index 081e6238..9f25ff7a 100644 --- a/src/lib/Hydra/Schema/Result/Builds.pm +++ b/src/lib/Hydra/Schema/Result/Builds.pm @@ -499,6 +499,21 @@ __PACKAGE__->belongs_to( { is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" }, ); +=head2 runcommandlogs + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "runcommandlogs", + "Hydra::Schema::Result::RunCommandLogs", + { "foreign.build_id" => "self.id" }, + undef, +); + =head2 aggregates Type: many_to_many @@ -528,8 +543,8 @@ __PACKAGE__->many_to_many( ); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WHdSVHhQykmUz0tR/TExVg +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-17 12:42:34 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ylttv/NTMDcSZumBXRCOCw __PACKAGE__->has_many( "dependents", diff --git a/src/lib/Hydra/Schema/Result/RunCommandLogs.pm b/src/lib/Hydra/Schema/Result/RunCommandLogs.pm new file mode 100644 index 00000000..b74416e8 --- /dev/null +++ b/src/lib/Hydra/Schema/Result/RunCommandLogs.pm @@ -0,0 +1,323 @@ +use utf8; +package Hydra::Schema::Result::RunCommandLogs; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Hydra::Schema::Result::RunCommandLogs + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("+Hydra::Component::ToJSON"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("runcommandlogs"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + sequence: 'runcommandlogs_id_seq' + +=head2 job_matcher + + data_type: 'text' + is_nullable: 0 + +=head2 build_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 command + + data_type: 'text' + is_nullable: 0 + +=head2 start_time + + data_type: 'integer' + is_nullable: 1 + +=head2 end_time + + data_type: 'integer' + is_nullable: 1 + +=head2 error_number + + data_type: 'integer' + is_nullable: 1 + +=head2 exit_code + + data_type: 'integer' + is_nullable: 1 + +=head2 signal + + data_type: 'integer' + is_nullable: 1 + +=head2 core_dumped + + data_type: 'boolean' + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "runcommandlogs_id_seq", + }, + "job_matcher", + { data_type => "text", is_nullable => 0 }, + "build_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "command", + { data_type => "text", is_nullable => 0 }, + "start_time", + { data_type => "integer", is_nullable => 1 }, + "end_time", + { data_type => "integer", is_nullable => 1 }, + "error_number", + { data_type => "integer", is_nullable => 1 }, + "exit_code", + { data_type => "integer", is_nullable => 1 }, + "signal", + { data_type => "integer", is_nullable => 1 }, + "core_dumped", + { data_type => "boolean", is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 build + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "build", + "Hydra::Schema::Result::Builds", + { id => "build_id" }, + { is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-19 15:15:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9AIzlQl1RjRXrs9gQCZKVw + +use POSIX qw(WEXITSTATUS WIFEXITED WIFSIGNALED WTERMSIG); + +=head2 started + +Update the row with the current timestamp as the start time. + +=cut +sub started { + my ($self) = @_; + + return $self->update({ + start_time => time() + }); +} + +=head2 completed_with_child_error + +Update the row with the current timestamp, exit code, core dump, errno, +and signal status. + +Arguments: + +=over 2 + +=item C<$child_error> + +The value of $? or $CHILD_ERROR (with use English) after calling system(). + +=item C<$errno> + +The value of $! or $ERRNO (with use English) after calling system(). + +=back + +=cut +sub completed_with_child_error { + my ($self, $child_error, $reported_errno) = @_; + + my $errno = undef; + my $exit_code = undef; + my $signal = undef; + my $core_dumped = undef; + + if ($child_error == -1) { + # -1 indicates `exec` failed, and this is the only + # case where the reported errno is valid. + # + # The `+ 0` is because $! is a dual var and likes to be a string + # if it can. +0 forces it to not be. Sigh. + $errno = $reported_errno + 0; + } + + if (WIFEXITED($child_error)) { + # The exit status bits are only meaningful if the process exited + $exit_code = WEXITSTATUS($child_error); + } + + if (WIFSIGNALED($child_error)) { + # The core dump and signal bits are only meaningful if the + # process was terminated via a signal + $signal = WTERMSIG($child_error); + + # This `& 128` comes from where Perl constructs the CHILD_ERROR + # value: + # https://github.com/Perl/perl5/blob/a9d7a07c2ebbfd8ee992f1d27ef4cfbed53085b6/perl.h#L3609-L3621 + # + # The `+ 0` is handling another dualvar. It is a bool, but a + # bool false is an empty string in boolean context and 0 in a + # numeric concept. The ORM knows the column is a bool, but + # does not treat the empty string as a bool when talking to + # postgres. + $core_dumped = (($child_error & 128) == 128) + 0; + } + + return $self->update({ + end_time => time(), + error_number => $errno, + exit_code => $exit_code, + signal => $signal, + core_dumped => $core_dumped, + }); +} + +=head2 did_succeed + +Return: + +* true if the task ran and finished successfully, +* false if the task did not run successfully but is completed +* undef if the task has not yet run + +=cut +sub did_succeed { + my ($self) = @_; + + if (!defined($self->end_time)) { + return undef; + } + + if (!defined($self->exit_code)) { + return 0; + } + + return $self->exit_code == 0; +} + + +=head2 is_running + +Looks in the database to see if the task has been marked as completed. +Does not actually examine to see if the process is running anywhere. + +Return: + +* true if the task does not have a marked end date +* false if the task does have a recorded end +=cut +sub is_running { + my ($self) = @_; + + return !defined($self->end_time); +} + +=head2 did_fail_with_signal + +Looks in the database to see if the task failed with a signal. + +Return: + +* true if the task is not running and failed with a signal. +* false if the task is running or exited with an exit code. +=cut +sub did_fail_with_signal { + my ($self) = @_; + + if ($self->is_running()) { + return 0; + } + + if ($self->did_succeed()) { + return 0; + } + + return defined($self->signal); +} + +=head2 did_fail_with_exec_error + +Looks in the database to see if the task failed with a signal. + +Return: + +* true if the task is not running and failed with a signal. +* false if the task is running or exited with an exit code. +=cut +sub did_fail_with_exec_error { + my ($self) = @_; + + if ($self->is_running()) { + return 0; + } + + if ($self->did_succeed()) { + return 0; + } + + return defined($self->error_number); +} + +1; diff --git a/src/root/build.tt b/src/root/build.tt index 30014dec..dc58f191 100644 --- a/src/root/build.tt +++ b/src/root/build.tt @@ -149,6 +149,7 @@ END; [% IF build.dependents %][% END%] [% IF drvAvailable %][% END %] [% IF localStore && available %][% END %] + [% IF runcommandlogs.size() > 0 %][% END %]
@@ -477,7 +478,7 @@ END;
- [% END %] +[% END %] [% IF drvAvailable %] [% INCLUDE makeLazyTab tabName="tabs-build-deps" uri=c.uri_for('/build' build.id 'build-deps') %] @@ -487,6 +488,56 @@ END; [% INCLUDE makeLazyTab tabName="tabs-runtime-deps" uri=c.uri_for('/build' build.id 'runtime-deps') %] [% END %] +
+
+ [% FOREACH runcommandlog IN runcommandlogs %] +
+
+
+ [% IF runcommandlog.did_succeed() %] + Succeeded + [% ELSIF runcommandlog.is_running() %] + + [% ELSE %] + Failed + [% END %] +
+ +
+
[% runcommandlog.command | html%]
+
+ [% IF not runcommandlog.is_running() %] + [% IF runcommandlog.did_fail_with_signal() %] + Exit signal: [% runcommandlog.signal %] + [% IF runcommandlog.core_dumped %] + (Core Dumped) + [% END %] + [% ELSIF runcommandlog.did_fail_with_exec_error() %] + Exec error: [% runcommandlog.error_number %] + [% ELSIF not runcommandlog.did_succeed() %] + Exit code: [% runcommandlog.exit_code %] + [% END %] + [% END %] +
+
+ +
+ [% IF runcommandlog.start_time != undef %] +
Started at [% INCLUDE renderDateTime timestamp = runcommandlog.start_time; %]
+ [% IF runcommandlog.end_time != undef %] +
Ran for [% INCLUDE renderDuration duration = runcommandlog.end_time - runcommandlog.start_time %]
+ [% ELSE %] +
Running for [% INCLUDE renderDuration duration = curTime - runcommandlog.start_time %]
+ [% END %] + [% ELSE %] +
Pending
+ [% END %] +
+
+
+ [% END %] +
+