mirror of
https://github.com/pragma-/pbot.git
synced 2024-11-08 13:09:34 +01:00
1326b0ac5f
VM socket communication is superior to VM serial communication in every way. Unfortunately at this time only Linux supports them. Fortunately, that's 99% of PBot's userbase. If you're not using Linux or if you're using an older Linux that does not support VM sockets, the PBot VM scripts will gracefully fallback to using the serial connection. You may explicitly disable VM socket connection attempts by setting PBOTVM_CID=0.
264 lines
6.2 KiB
Perl
Executable File
264 lines
6.2 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# File: vm-server
|
|
#
|
|
# Purpose: The PBot Host Server manages the guest virtual machine state and
|
|
# listens for incoming commands from vm-client. Invokes vm-exec to send
|
|
# commands to the PBot Guest Server (guest-server).
|
|
|
|
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
use 5.020;
|
|
|
|
use warnings;
|
|
use strict;
|
|
|
|
use feature qw(signatures);
|
|
no warnings qw(experimental::signatures);
|
|
|
|
use IO::Socket;
|
|
use Net::hostent;
|
|
use IPC::Shareable;
|
|
use Time::HiRes qw/gettimeofday/;
|
|
use Encode;
|
|
|
|
use constant {
|
|
SERVER_PORT => $ENV{PBOTVM_SERVER} // 9000,
|
|
HEARTBEAT_PORT => $ENV{PBOTVM_HEART} // 5556,
|
|
DOMAIN_NAME => $ENV{PBOTVM_DOMAIN} // 'pbot-vm',
|
|
COMPILE_TIMEOUT => $ENV{PBOTVM_TIMEOUT} // 10,
|
|
};
|
|
|
|
sub vm_revert() {
|
|
return if $ENV{PBOTVM_NOREVERT};
|
|
print "Reverting vm...\n";
|
|
system('time virsh snapshot-revert '.DOMAIN_NAME.' 1');
|
|
print "Reverted.\n";
|
|
}
|
|
|
|
sub execute($command) {
|
|
print "execute ($command)\n";
|
|
|
|
# to get $? from pipe
|
|
local $SIG{CHLD} = 'DEFAULT';
|
|
|
|
my $pid = open(my $fh, '-|', split / /, $command);
|
|
|
|
if (not defined $pid) {
|
|
print "Couldn't fork: $!\n";
|
|
return (-13, "[Fatal error]");
|
|
}
|
|
|
|
my $result = eval {
|
|
my $output = '';
|
|
local $SIG{ALRM} = sub { kill 9, $pid; die "Timed-out: $output\n"; };
|
|
alarm(COMPILE_TIMEOUT);
|
|
|
|
while (my $line = <$fh>) {
|
|
$output .= $line;
|
|
}
|
|
|
|
return $output;
|
|
};
|
|
|
|
alarm 0;
|
|
close $fh;
|
|
|
|
my $ret = $? >> 8;
|
|
|
|
if (my $exception = $@) {
|
|
# handle time-out exception
|
|
if ($exception =~ /Timed-out: (.*)/) {
|
|
return (-13, "[Timed-out] $1");
|
|
}
|
|
|
|
# propagate unhandled exception
|
|
die $exception;
|
|
}
|
|
|
|
return ($ret, $result);
|
|
}
|
|
|
|
sub connect_to_heartbeat() {
|
|
my $heartbeat;
|
|
my $attempts = 15;
|
|
|
|
while (!$heartbeat && $attempts > 0) {
|
|
print "Connecting to heartbeat on port ".HEARTBEAT_PORT." ... ";
|
|
|
|
$heartbeat = IO::Socket::INET->new (
|
|
PeerAddr => '127.0.0.1',
|
|
PeerPort => HEARTBEAT_PORT,
|
|
Proto => 'tcp',
|
|
Type => SOCK_STREAM,
|
|
);
|
|
|
|
if (!$heartbeat) {
|
|
print "failed.\n";
|
|
--$attempts;
|
|
print "Trying again in 2 seconds ($attempts attempts remaining) ...\n" if $attempts > 0;
|
|
sleep 2;
|
|
} else {
|
|
print "success!\n";
|
|
}
|
|
}
|
|
|
|
return $heartbeat;
|
|
}
|
|
|
|
sub do_heartbeat() {
|
|
tie my $heartbeat, 'IPC::Shareable', { key => 'heartbeat' };
|
|
tie my $running, 'IPC::Shareable', { key => 'running' };
|
|
|
|
while ($running) {
|
|
my $heartbeat_monitor = connect_to_heartbeat();
|
|
|
|
while ($running and <$heartbeat_monitor>) {
|
|
$heartbeat = time;
|
|
}
|
|
}
|
|
exit;
|
|
}
|
|
|
|
sub server_listen($port) {
|
|
my $server = IO::Socket::INET->new (
|
|
Proto => 'tcp',
|
|
LocalPort => $port,
|
|
Listen => SOMAXCONN,
|
|
ReuseAddr => 1,
|
|
Reuse => 1,
|
|
);
|
|
die "Can't setup server: $!" unless $server;
|
|
print "Server $0 accepting clients at :$port\n";
|
|
return $server;
|
|
}
|
|
|
|
sub do_server() {
|
|
tie my $heartbeat, 'IPC::Shareable', { key => 'heartbeat' };
|
|
tie my $running, 'IPC::Shareable', { key => 'running' };
|
|
|
|
print "Starting PBot VM Server on port " . SERVER_PORT . "\n";
|
|
my $server = eval { server_listen(SERVER_PORT) };
|
|
|
|
if ($@) {
|
|
print STDERR $@;
|
|
$running = 0;
|
|
return;
|
|
}
|
|
|
|
while ($running and my $client = $server->accept) {
|
|
print '-' x 20, "\n";
|
|
my $hostinfo = gethostbyaddr($client->peeraddr);
|
|
print "Connect from ", $client->peerhost, " at ", scalar localtime, "\n";
|
|
handle_client($client, $heartbeat);
|
|
}
|
|
|
|
print "Shutting down server.\n";
|
|
}
|
|
|
|
sub handle_client($client, $heartbeat) {
|
|
my ($timed_out, $killed) = (0, 0);
|
|
|
|
# we don't care about child exit status
|
|
local $SIG{CHLD} = 'IGNORE';
|
|
|
|
my $r = fork;
|
|
|
|
if (not defined $r) {
|
|
print "Could not fork to handle client: $!\n";
|
|
print $client "Fatal error.\n";
|
|
close $client;
|
|
return;
|
|
}
|
|
|
|
if ($r > 0) {
|
|
# nothing for parent to do with client
|
|
close $client;
|
|
return;
|
|
}
|
|
|
|
$client->autoflush(1);
|
|
|
|
eval {
|
|
# give client 5 seconds to send a line
|
|
local $SIG{ALRM} = sub { die "Client I/O timed-out\n"; };
|
|
alarm 5;
|
|
|
|
while (my $line = <$client>) {
|
|
$line =~ s/[\r\n]+$//;
|
|
next if $line =~ m/^\s*$/;
|
|
|
|
# give client 5 more seconds
|
|
alarm 5;
|
|
|
|
print "[$$] Read [$line]\n";
|
|
|
|
if (time - $heartbeat > 5) {
|
|
print "[$$] Lost heartbeat, ignoring compile attempt.\n";
|
|
print $client "Virtual machine is resetting, try again soon.\n";
|
|
last;
|
|
}
|
|
|
|
# disable client time-out
|
|
alarm 0;
|
|
|
|
my ($ret, $result) = execute("perl vm-exec $line");
|
|
|
|
$result =~ s/\s+$//;
|
|
print "Ret: $ret; result: [$result]\n";
|
|
|
|
if ($result =~ m/\[Killed\]$/) {
|
|
$killed = 1;
|
|
$ret = -14;
|
|
}
|
|
|
|
if ($ret == -13 && $result =~ m/\[Timed-out\]/) {
|
|
$timed_out = 1;
|
|
}
|
|
|
|
print $client $result . "\n";
|
|
last;
|
|
}
|
|
};
|
|
|
|
# print client time-out exception
|
|
print "[$$] $@" if $@;
|
|
|
|
alarm 0;
|
|
close $client;
|
|
|
|
print "[$$] timed out: $timed_out; killed: $killed\n";
|
|
|
|
if ($timed_out || $killed) {
|
|
vm_revert();
|
|
}
|
|
|
|
# child done
|
|
print "[$$] client exiting\n";
|
|
print "=" x 20, "\n";
|
|
exit;
|
|
}
|
|
|
|
sub main() {
|
|
tie my $heartbeat, 'IPC::Shareable', { key => 'heartbeat', create => 1, destroy => 1 };
|
|
tie my $running, 'IPC::Shareable', { key => 'running', create => 1, destroy => 1 };
|
|
|
|
$running = 1;
|
|
$heartbeat = 0;
|
|
|
|
my $heartbeat_pid = fork // die "Heartbeat fork failed: $!";
|
|
|
|
if ($heartbeat_pid == 0) {
|
|
do_heartbeat();
|
|
} else {
|
|
do_server();
|
|
}
|
|
|
|
print "Waiting for heart to stop...\n";
|
|
waitpid($heartbeat_pid, 0);
|
|
print "Heart stopped.\n";
|
|
}
|
|
|
|
main();
|