2020-02-15 03:52:41 +01:00
|
|
|
# File: ProcessManager.pm
|
2010-03-22 08:33:44 +01:00
|
|
|
#
|
2021-06-19 06:23:34 +02:00
|
|
|
# Purpose: Handles forking and execution of module/subroutine processes.
|
|
|
|
# Provides commands to list running processes and to kill them.
|
2010-03-22 08:33:44 +01:00
|
|
|
|
2021-07-11 00:00:22 +02:00
|
|
|
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
|
|
|
|
# SPDX-License-Identifier: MIT
|
2017-03-05 22:33:31 +01:00
|
|
|
|
2021-07-21 07:44:51 +02:00
|
|
|
package PBot::Core::ProcessManager;
|
|
|
|
use parent 'PBot::Core::Class';
|
2010-03-22 08:33:44 +01:00
|
|
|
|
2021-06-19 06:23:34 +02:00
|
|
|
use PBot::Imports;
|
2019-07-11 03:40:53 +02:00
|
|
|
|
2020-02-17 02:45:45 +01:00
|
|
|
use Time::HiRes qw/gettimeofday/;
|
2020-02-16 20:03:25 +01:00
|
|
|
use POSIX qw/WNOHANG/;
|
2017-10-11 02:19:02 +02:00
|
|
|
use JSON;
|
2010-03-22 08:33:44 +01:00
|
|
|
|
|
|
|
sub initialize {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, %conf) = @_;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# hash of currently running bot-invoked processes
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{processes} = {};
|
|
|
|
|
|
|
|
# automatically reap children processes in background
|
|
|
|
$SIG{CHLD} = sub {
|
|
|
|
my $pid; do { $pid = waitpid(-1, WNOHANG); $self->remove_process($pid) if $pid > 0; } while $pid > 0;
|
|
|
|
};
|
2010-03-22 08:33:44 +01:00
|
|
|
}
|
|
|
|
|
2020-02-15 03:52:41 +01:00
|
|
|
sub add_process {
|
2020-05-02 05:59:51 +02:00
|
|
|
my ($self, $pid, $context) = @_;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{process_start} = gettimeofday;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$self->{processes}->{$pid} = $context;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$self->{pbot}->{logger}->log("Starting process $pid: $context->{commands}->[0]\n");
|
2020-02-15 03:52:41 +01:00
|
|
|
}
|
2017-10-11 02:19:02 +02:00
|
|
|
|
2020-02-15 03:52:41 +01:00
|
|
|
sub remove_process {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $pid) = @_;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-17 02:45:45 +01:00
|
|
|
if (exists $self->{processes}->{$pid}) {
|
|
|
|
my $command = $self->{processes}->{$pid}->{commands}->[0];
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-17 02:45:45 +01:00
|
|
|
my $duration = gettimeofday - $self->{processes}->{$pid}->{process_start};
|
|
|
|
$duration = sprintf "%0.3f", $duration;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-17 02:45:45 +01:00
|
|
|
$self->{pbot}->{logger}->log("Finished process $pid ($command): duration $duration seconds\n");
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-17 02:45:45 +01:00
|
|
|
delete $self->{processes}->{$pid};
|
|
|
|
} else {
|
2020-02-20 17:20:54 +01:00
|
|
|
$self->{pbot}->{logger}->log("Finished process $pid\n");
|
2020-02-17 02:45:45 +01:00
|
|
|
}
|
2020-02-15 03:52:41 +01:00
|
|
|
}
|
2010-05-14 01:28:38 +02:00
|
|
|
|
2020-02-15 03:52:41 +01:00
|
|
|
sub execute_process {
|
2020-05-02 05:59:51 +02:00
|
|
|
my ($self, $context, $subref, $timeout) = @_;
|
2020-02-15 23:38:32 +01:00
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
$timeout //= 30; # default timeout 30 seconds
|
|
|
|
|
2021-06-11 23:58:16 +02:00
|
|
|
# ensure contextual command history list is available for add_process()
|
2021-06-07 04:12:14 +02:00
|
|
|
if (not exists $context->{commands}) {
|
|
|
|
$context->{commands} = [$context->{command}];
|
|
|
|
}
|
2020-02-15 23:38:32 +01:00
|
|
|
|
2020-02-17 01:31:06 +01:00
|
|
|
# don't fork again if we're already a forked process
|
2021-06-07 04:12:14 +02:00
|
|
|
if (defined $context->{pid} and $context->{pid} == 0) {
|
2020-05-02 05:59:51 +02:00
|
|
|
$subref->($context);
|
|
|
|
return $context->{result};
|
2020-02-17 01:31:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
pipe(my $reader, my $writer);
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# fork new process
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{pid} = fork;
|
2019-07-06 08:00:53 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
if (not defined $context->{pid}) {
|
2021-06-07 04:12:14 +02:00
|
|
|
# fork failed
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{pbot}->{logger}->log("Could not fork process: $!\n");
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
close $reader;
|
|
|
|
close $writer;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
delete $context->{pid};
|
|
|
|
|
|
|
|
# groan to let the users know something went wrong
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{checkflood} = 1;
|
|
|
|
$self->{pbot}->{interpreter}->handle_result($context, "/me groans loudly.\n");
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
return;
|
2019-07-06 08:00:53 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
if ($context->{pid} == 0) {
|
2020-02-15 23:38:32 +01:00
|
|
|
# child
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
close $reader;
|
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# flag this instance as child
|
2020-09-13 01:28:44 +02:00
|
|
|
$self->{pbot}->{child} = 1;
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
# don't quit the IRC client when the child dies
|
|
|
|
no warnings;
|
2021-07-21 07:44:51 +02:00
|
|
|
*PBot::Core::IRC::Connection::DESTROY = sub { return; };
|
2020-02-15 23:38:32 +01:00
|
|
|
use warnings;
|
|
|
|
|
|
|
|
# remove atexit handlers
|
|
|
|
$self->{pbot}->{atexit}->unregister_all;
|
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
# execute the provided subroutine, results are stored in $context
|
2020-02-15 23:38:32 +01:00
|
|
|
eval {
|
2020-05-02 05:59:51 +02:00
|
|
|
local $SIG{ALRM} = sub { die "Process `$context->{commands}->[0]` timed-out" };
|
2020-02-15 23:38:32 +01:00
|
|
|
alarm $timeout;
|
2020-05-02 05:59:51 +02:00
|
|
|
$subref->($context);
|
2021-06-22 02:26:24 +02:00
|
|
|
alarm 0;
|
2020-02-15 23:38:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
# check for errors
|
|
|
|
if ($@) {
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{result} = $@;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{'timed-out'} = 1 if $context->{result} =~ /^Process .* timed-out at PBot\/ProcessManager/;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$self->{pbot}->{logger}->log("Error executing process: $context->{result}\n");
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# strip internal PBot source data for IRC output
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{result} =~ s/ at PBot.*$//ms;
|
2020-09-27 01:01:52 +02:00
|
|
|
$context->{result} =~ s/\s+...propagated at .*$//ms;
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
# print $context to pipe
|
|
|
|
my $json = encode_json $context;
|
2020-02-15 23:38:32 +01:00
|
|
|
print $writer "$json\n";
|
2020-09-29 21:29:40 +02:00
|
|
|
close $writer;
|
2020-02-15 23:38:32 +01:00
|
|
|
|
|
|
|
# end child
|
|
|
|
exit 0;
|
|
|
|
} else {
|
|
|
|
# parent
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# nothing to write to child
|
2020-02-15 23:38:32 +01:00
|
|
|
close $writer;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# add process
|
2020-05-02 05:59:51 +02:00
|
|
|
$self->add_process($context->{pid}, $context);
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
# add reader handler
|
2020-05-02 05:59:51 +02:00
|
|
|
$self->{pbot}->{select_handler}->add_reader($reader, sub { $self->process_pipe_reader($context->{pid}, @_) });
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
# return empty string since reader will handle the output when child is finished
|
2021-06-07 04:12:14 +02:00
|
|
|
return '';
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
2014-03-14 11:05:11 +01:00
|
|
|
}
|
|
|
|
|
2020-02-15 03:52:41 +01:00
|
|
|
sub process_pipe_reader {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $pid, $buf) = @_;
|
2020-09-29 21:29:40 +02:00
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# retrieve context object from child
|
2020-05-02 05:59:51 +02:00
|
|
|
my $context = decode_json $buf or do {
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{pbot}->{logger}->log("Failed to decode bad json: [$buf]\n");
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# context is no longer forked
|
2020-05-02 05:59:51 +02:00
|
|
|
delete $context->{pid};
|
2020-02-19 05:11:39 +01:00
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# check for output
|
2020-05-02 05:59:51 +02:00
|
|
|
if (not defined $context->{result} or not length $context->{result}) {
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{pbot}->{logger}->log("No result from process.\n");
|
2020-05-05 01:51:55 +02:00
|
|
|
return if $context->{suppress_no_output};
|
|
|
|
$context->{result} = "No output.";
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
|
|
|
|
2021-07-24 03:26:45 +02:00
|
|
|
# don't output unnecessary result if command was embedded within a message
|
|
|
|
if ($context->{embedded}) {
|
2021-06-07 04:12:14 +02:00
|
|
|
return if $context->{result} =~ m/(?:no results)/i;
|
|
|
|
}
|
2020-02-15 23:38:32 +01:00
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# handle code factoid result
|
2020-05-02 05:59:51 +02:00
|
|
|
if (exists $context->{special} and $context->{special} eq 'code-factoid') {
|
|
|
|
$context->{result} =~ s/\s+$//g;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
|
|
|
if (not length $context->{result}) {
|
|
|
|
$self->{pbot}->{logger}->log("No text result from code-factoid.\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{original_keyword} = $context->{root_keyword};
|
2021-06-07 04:12:14 +02:00
|
|
|
$context->{result} = $self->{pbot}->{factoids}->handle_action($context, $context->{result});
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# if nick isn't overridden yet, check for a potential nick prefix
|
2021-06-11 23:58:16 +02:00
|
|
|
# TODO: this stuff should be moved to Interpreter::output_result
|
|
|
|
if (not $context->{nickprefix}) {
|
2021-07-13 23:37:18 +02:00
|
|
|
$context->{trigger} //= '';
|
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# if add_nick is set on the factoid, set the nick override to the caller's nick
|
|
|
|
if (exists $context->{special} and $context->{special} ne 'code-factoid'
|
2021-07-09 23:39:35 +02:00
|
|
|
and $self->{pbot}->{factoids}->{storage}->exists($context->{channel}, $context->{trigger}, 'add_nick')
|
|
|
|
and $self->{pbot}->{factoids}->{storage}->get_data($context->{channel}, $context->{trigger}, 'add_nick') != 0)
|
2020-02-15 23:38:32 +01:00
|
|
|
{
|
2021-06-11 23:58:16 +02:00
|
|
|
$context->{nickprefix} = $context->{nick};
|
|
|
|
$context->{nickprefix_disabled} = 0;
|
|
|
|
$context->{nickprefix_forced} = 1;
|
2017-11-26 05:00:55 +01:00
|
|
|
} else {
|
2021-06-07 04:12:14 +02:00
|
|
|
# extract nick-like thing from process result
|
2020-05-02 05:59:51 +02:00
|
|
|
if ($context->{result} =~ s/^(\S+): //) {
|
2020-02-15 23:38:32 +01:00
|
|
|
my $nick = $1;
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if (lc $nick eq "usage") {
|
|
|
|
# put it back on result if it's a usage message
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{result} = "$nick: $context->{result}";
|
2020-02-15 23:38:32 +01:00
|
|
|
} else {
|
2020-05-02 05:59:51 +02:00
|
|
|
my $present = $self->{pbot}->{nicklist}->is_present($context->{channel}, $nick);
|
2021-06-07 04:12:14 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if ($present) {
|
|
|
|
# nick is present in channel
|
2021-06-11 23:58:16 +02:00
|
|
|
$context->{nickprefix} = $present;
|
2020-02-15 23:38:32 +01:00
|
|
|
} else {
|
|
|
|
# nick not present, put it back on result
|
2020-05-02 05:59:51 +02:00
|
|
|
$context->{result} = "$nick: $context->{result}";
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-26 05:00:55 +01:00
|
|
|
}
|
2017-10-11 02:19:02 +02:00
|
|
|
}
|
|
|
|
|
2021-06-07 04:12:14 +02:00
|
|
|
# send the result off to the bot to be handled
|
|
|
|
$context->{checkflood} = 1;
|
2021-06-11 23:58:16 +02:00
|
|
|
$self->{pbot}->{interpreter}->handle_result($context);
|
2010-03-22 08:33:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|