refixed the server query
This commit is contained in:
parent
eedc3e8fb3
commit
e2dd5a496c
8 changed files with 539 additions and 20 deletions
|
|
@ -5,5 +5,7 @@
|
|||
ftp_method => 'PureFTPd',
|
||||
ogp_autorestart_server => '1',
|
||||
protocol_shutdown_waittime => '10',
|
||||
PortValidationEnabled => '1',
|
||||
StartupValidationTimeoutSeconds => '180',
|
||||
PortCheckIntervalSeconds => '5',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ The updater downloads to a temporary file, rejects empty files, HTML error pages
|
|||
- Main log: `C:\\OGP\\ogp_agent.log`
|
||||
- PID files: `ogp_agent_run.pid` (wrapper) and `ogp_agent.pid` (Perl daemon)
|
||||
- Customer servers run inside GNU Screen sessions—attach via `C:\\OGP\\bin\\screen -r ogp_agent`
|
||||
- Server readiness uses the `server_status` RPC and validates only the game/query/RCON ports supplied by the Panel.
|
||||
- Port validation settings live in `/OGP/Cfg/Preferences.pm`: `PortValidationEnabled`, `StartupValidationTimeoutSeconds`, and `PortCheckIntervalSeconds`.
|
||||
- Port validation smoke test: `bash /OGP/tests/port_validation_smoke.sh 2302/udp 2303/udp`.
|
||||
- Firewall: open TCP 12679 (or your configured port) and any game-specific ports before provisioning.
|
||||
- Authentication errors almost always mean the `key` in `Cfg/Config.pm` does not match the value stored in the panel → Administration → Servers.
|
||||
- `/OGP/Cfg/bash_prefs.cfg` must use LF line endings and no leading whitespace before assignments. The launcher normalizes this automatically before sourcing the file.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ use constant WEB_API_URL => $Cfg::Config{web_api_url};
|
|||
use constant STEAM_DL_LIMIT => $Cfg::Config{steam_dl_limit};
|
||||
use constant SCREEN_LOG_LOCAL => $Cfg::Preferences{screen_log_local};
|
||||
use constant DELETE_LOGS_AFTER => $Cfg::Preferences{delete_logs_after};
|
||||
use constant PORT_VALIDATION_ENABLED =>
|
||||
defined($Cfg::Preferences{PortValidationEnabled}) ? $Cfg::Preferences{PortValidationEnabled} : 1;
|
||||
use constant STARTUP_VALIDATION_TIMEOUT =>
|
||||
defined($Cfg::Preferences{StartupValidationTimeoutSeconds}) ? $Cfg::Preferences{StartupValidationTimeoutSeconds} : 180;
|
||||
use constant PORT_CHECK_INTERVAL_SECONDS =>
|
||||
defined($Cfg::Preferences{PortCheckIntervalSeconds}) ? $Cfg::Preferences{PortCheckIntervalSeconds} : 5;
|
||||
use constant AGENT_PID_FILE =>
|
||||
Path::Class::File->new(AGENT_RUN_DIR, 'ogp_agent.pid');
|
||||
use constant AGENT_RSYNC_GENERIC_LOG =>
|
||||
|
|
@ -746,77 +752,277 @@ sub get_screen_pid_without_decrypt
|
|||
return "";
|
||||
}
|
||||
|
||||
sub add_expected_port
|
||||
{
|
||||
my ($ports, $seen, $name, $port, $protocol) = @_;
|
||||
return unless(defined($port) && $port =~ /^[0-9]+$/ && $port > 0 && $port <= 65535);
|
||||
$protocol = "any" unless(defined($protocol) && $protocol =~ /^(tcp|udp|any)$/i);
|
||||
$protocol = lc($protocol);
|
||||
my $key = $protocol . ":" . $port;
|
||||
return if($seen->{$key});
|
||||
$seen->{$key} = 1;
|
||||
push(@$ports, {name => $name, port => int($port), protocol => $protocol});
|
||||
}
|
||||
|
||||
sub parse_port_list
|
||||
{
|
||||
my ($ports, $seen, $name, $value) = @_;
|
||||
return unless(defined($value) && $value ne "");
|
||||
foreach my $part (split(/[,\s;]+/, $value))
|
||||
{
|
||||
next if($part eq "");
|
||||
if($part =~ /^([0-9]{1,5})(?:\/(tcp|udp|any))?$/i ||
|
||||
$part =~ /^(tcp|udp|any):([0-9]{1,5})$/i)
|
||||
{
|
||||
my ($port, $protocol);
|
||||
if($part =~ /^([0-9]{1,5})(?:\/(tcp|udp|any))?$/i)
|
||||
{
|
||||
($port, $protocol) = ($1, $2 || "any");
|
||||
}
|
||||
else
|
||||
{
|
||||
($protocol, $port) = ($1, $2);
|
||||
}
|
||||
add_expected_port($ports, $seen, $name, $port, $protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub build_expected_ports
|
||||
{
|
||||
my ($server_port, $query_port, $rcon_port) = @_;
|
||||
my @ports;
|
||||
my %seen;
|
||||
parse_port_list(\@ports, \%seen, "game", $server_port);
|
||||
parse_port_list(\@ports, \%seen, "query", $query_port);
|
||||
parse_port_list(\@ports, \%seen, "rcon", $rcon_port);
|
||||
return @ports;
|
||||
}
|
||||
|
||||
sub collect_listening_ports_without_decrypt
|
||||
{
|
||||
my (%listening_tcp, %listening_udp);
|
||||
my $ps_cmd = q!powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\$p=[System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties();\$p.GetActiveTcpListeners()|ForEach-Object{'TCP '+\$_.Port};\$p.GetActiveUdpListeners()|ForEach-Object{'UDP '+\$_.Port}" 2>/dev/null!;
|
||||
my $out = `$ps_cmd`;
|
||||
|
||||
if(defined($out) && $out =~ /\S/)
|
||||
{
|
||||
foreach my $line (split(/\r?\n/, $out))
|
||||
{
|
||||
$listening_tcp{$1} = 1 if($line =~ /^TCP\s+([0-9]{1,5})$/i);
|
||||
$listening_udp{$1} = 1 if($line =~ /^UDP\s+([0-9]{1,5})$/i);
|
||||
}
|
||||
return (\%listening_tcp, \%listening_udp) if(keys(%listening_tcp) || keys(%listening_udp));
|
||||
}
|
||||
|
||||
$out = `netstat -an 2>/dev/null`;
|
||||
foreach my $line (split(/\r?\n/, $out))
|
||||
{
|
||||
if($line =~ /^\s*TCP\s+\S+[:.]([0-9]{1,5})\s+\S+\s+LISTENING/i)
|
||||
{
|
||||
$listening_tcp{$1} = 1;
|
||||
}
|
||||
elsif($line =~ /^\s*UDP\s+\S+[:.]([0-9]{1,5})\s+/i)
|
||||
{
|
||||
$listening_udp{$1} = 1;
|
||||
}
|
||||
}
|
||||
return (\%listening_tcp, \%listening_udp);
|
||||
}
|
||||
|
||||
sub validate_expected_ports
|
||||
{
|
||||
my (@expected_ports) = @_;
|
||||
my ($listening_tcp, $listening_udp) = collect_listening_ports_without_decrypt();
|
||||
my (@listening, @missing);
|
||||
|
||||
foreach my $expected (@expected_ports)
|
||||
{
|
||||
my $port = $expected->{port};
|
||||
my $protocol = $expected->{protocol};
|
||||
my $found = 0;
|
||||
$found = 1 if(($protocol eq "tcp" || $protocol eq "any") && $listening_tcp->{$port});
|
||||
$found = 1 if(($protocol eq "udp" || $protocol eq "any") && $listening_udp->{$port});
|
||||
if($found)
|
||||
{
|
||||
push(@listening, $expected);
|
||||
}
|
||||
else
|
||||
{
|
||||
push(@missing, $expected);
|
||||
}
|
||||
}
|
||||
return (\@listening, \@missing);
|
||||
}
|
||||
|
||||
sub has_listening_port_named
|
||||
{
|
||||
my ($ports, $name) = @_;
|
||||
foreach my $port (@$ports)
|
||||
{
|
||||
return 1 if($port->{name} eq $name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub is_port_listening_without_decrypt
|
||||
{
|
||||
my ($server_ip, $port) = @_;
|
||||
my ($server_ip, $port, $protocol) = @_;
|
||||
return 0 unless(defined($port) && $port =~ /^[0-9]+$/ && $port > 0 && $port <= 65535);
|
||||
my $out = `netstat -an 2>/dev/null`;
|
||||
return 1 if(defined($out) && $out =~ /^\s*TCP\s+\S+[:.]$port\s+\S+\s+LISTENING/im);
|
||||
return 1 if(defined($out) && $out =~ /^\s*UDP\s+\S+[:.]$port\s+/im);
|
||||
$protocol = "any" unless(defined($protocol) && $protocol =~ /^(tcp|udp|any)$/i);
|
||||
my ($listening_tcp, $listening_udp) = collect_listening_ports_without_decrypt();
|
||||
return 1 if(($protocol eq "tcp" || $protocol eq "any") && $listening_tcp->{$port});
|
||||
return 1 if(($protocol eq "udp" || $protocol eq "any") && $listening_udp->{$port});
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub get_agent_cpu_usage_percent
|
||||
{
|
||||
open(STAT, '/proc/stat') or return "";
|
||||
my $line = <STAT>;
|
||||
close STAT;
|
||||
return "" unless(defined($line) && $line =~ /^cpu\s+/);
|
||||
my @first = split(/\s+/, $line);
|
||||
sleep 1;
|
||||
open(STAT, '/proc/stat') or return "";
|
||||
$line = <STAT>;
|
||||
close STAT;
|
||||
return "" unless(defined($line) && $line =~ /^cpu\s+/);
|
||||
my @second = split(/\s+/, $line);
|
||||
my $idle_diff = ($second[4] || 0) - ($first[4] || 0);
|
||||
my $total_first = 0;
|
||||
my $total_second = 0;
|
||||
for(my $i = 1; $i < @first; $i++) { $total_first += $first[$i] || 0; }
|
||||
for(my $i = 1; $i < @second; $i++) { $total_second += $second[$i] || 0; }
|
||||
my $total_diff = $total_second - $total_first;
|
||||
return "" if($total_diff <= 0);
|
||||
return sprintf("%.2f", (100 * ($total_diff - $idle_diff)) / $total_diff);
|
||||
}
|
||||
|
||||
sub get_agent_memory_usage_percent
|
||||
{
|
||||
my($total, $available, $free, $buffers, $cached) = qw(0 0 0 0 0);
|
||||
open(STAT, '/proc/meminfo') or return "";
|
||||
while (<STAT>)
|
||||
{
|
||||
$total += $1 if /MemTotal:\s+(\d+) kB/;
|
||||
$available += $1 if /MemAvailable:\s+(\d+) kB/;
|
||||
$free += $1 if /MemFree:\s+(\d+) kB/;
|
||||
$buffers += $1 if /Buffers:\s+(\d+) kB/;
|
||||
$cached += $1 if /^Cached:\s+(\d+) kB/;
|
||||
}
|
||||
close STAT;
|
||||
return "" if($total <= 0);
|
||||
my $used = $available > 0 ? $total - $available : $total - $free - $buffers - $cached;
|
||||
return sprintf("%.2f", (100 * $used) / $total);
|
||||
}
|
||||
|
||||
sub server_status_without_decrypt
|
||||
{
|
||||
my ($home_id, $server_ip, $server_port, $query_port, $rcon_port, $startup_timeout, $state_hint) = @_;
|
||||
$startup_timeout = 180 unless(defined($startup_timeout) && $startup_timeout =~ /^[0-9]+$/ && $startup_timeout > 0);
|
||||
$query_port = "" unless(defined($query_port) && $query_port =~ /^[0-9]+$/);
|
||||
$rcon_port = "" unless(defined($rcon_port) && $rcon_port =~ /^[0-9]+$/);
|
||||
$startup_timeout = STARTUP_VALIDATION_TIMEOUT unless(defined($startup_timeout) && $startup_timeout =~ /^[0-9]+$/ && $startup_timeout > 0);
|
||||
$query_port = "" unless(defined($query_port) && $query_port =~ /^[0-9,;:\s\/a-zA-Z]+$/);
|
||||
$rcon_port = "" unless(defined($rcon_port) && $rcon_port =~ /^[0-9,;:\s\/a-zA-Z]+$/);
|
||||
$state_hint = "" unless(defined($state_hint));
|
||||
|
||||
my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id);
|
||||
my $session_running = is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1 ? 1 : 0;
|
||||
my $pid = $session_running ? get_screen_pid_without_decrypt($home_id) : "";
|
||||
my $process_running = $session_running;
|
||||
my $game_port_listening = is_port_listening_without_decrypt($server_ip, $server_port);
|
||||
my $query_port_listening = $query_port ne "" ? is_port_listening_without_decrypt($server_ip, $query_port) : 0;
|
||||
my $rcon_port_listening = $rcon_port ne "" ? is_port_listening_without_decrypt($server_ip, $rcon_port) : 0;
|
||||
my @expected_ports = build_expected_ports($server_port, $query_port, $rcon_port);
|
||||
my ($listening_ports, $missing_ports) = PORT_VALIDATION_ENABLED ? validate_expected_ports(@expected_ports) : ([], []);
|
||||
my $expected_count = scalar(@expected_ports);
|
||||
my $listening_count = scalar(@$listening_ports);
|
||||
my $missing_count = scalar(@$missing_ports);
|
||||
my $game_port_listening = has_listening_port_named($listening_ports, "game");
|
||||
my $query_port_listening = has_listening_port_named($listening_ports, "query");
|
||||
my $rcon_port_listening = has_listening_port_named($listening_ports, "rcon");
|
||||
if(!PORT_VALIDATION_ENABLED)
|
||||
{
|
||||
$game_port_listening = is_port_listening_without_decrypt($server_ip, $server_port);
|
||||
$query_port_listening = $query_port ne "" ? is_port_listening_without_decrypt($server_ip, $query_port) : 0;
|
||||
$rcon_port_listening = $rcon_port ne "" ? is_port_listening_without_decrypt($server_ip, $rcon_port) : 0;
|
||||
}
|
||||
my ($hint_timestamp, $stored_hint) = read_status_hint($home_id);
|
||||
my $effective_hint = $state_hint ne "" ? $state_hint : $stored_hint;
|
||||
my $last_error = "";
|
||||
my $status = "UNKNOWN";
|
||||
my $status_state = "Unknown";
|
||||
my $ready = 0;
|
||||
|
||||
if($session_running && $game_port_listening)
|
||||
if(!$session_running && ((!PORT_VALIDATION_ENABLED && $game_port_listening) || ($expected_count > 0 && $listening_count > 0)))
|
||||
{
|
||||
$status = "ONLINE";
|
||||
$ready = 1;
|
||||
$status_state = $missing_count == 0 ? "Running" : "Warning";
|
||||
$last_error = "Required port is listening but the managed screen session is not running.";
|
||||
}
|
||||
elsif($session_running)
|
||||
{
|
||||
if($effective_hint eq "STOPPING")
|
||||
if(!PORT_VALIDATION_ENABLED && $game_port_listening)
|
||||
{
|
||||
$status = "ONLINE";
|
||||
$status_state = "Running";
|
||||
$ready = 1;
|
||||
}
|
||||
elsif(PORT_VALIDATION_ENABLED && $expected_count > 0 && $missing_count == 0)
|
||||
{
|
||||
$status = "ONLINE";
|
||||
$status_state = "Running";
|
||||
$ready = 1;
|
||||
}
|
||||
elsif(PORT_VALIDATION_ENABLED && $expected_count > 0 && $listening_count > 0)
|
||||
{
|
||||
$status = "ONLINE";
|
||||
$status_state = "Warning";
|
||||
$ready = 1;
|
||||
$last_error = "Process/session exists, but some required ports are not listening.";
|
||||
}
|
||||
elsif($effective_hint eq "STOPPING")
|
||||
{
|
||||
$status = "STOPPING";
|
||||
$status_state = "Starting";
|
||||
}
|
||||
elsif($hint_timestamp > 0 && (time() - $hint_timestamp) > $startup_timeout)
|
||||
{
|
||||
$status = "UNRESPONSIVE";
|
||||
$last_error = "Process/session exists but game port is not listening after startup timeout.";
|
||||
$status_state = "Failed";
|
||||
$last_error = "Process/session exists but no required ports are listening after startup timeout.";
|
||||
}
|
||||
else
|
||||
{
|
||||
$status = "STARTING";
|
||||
$status_state = "Starting";
|
||||
}
|
||||
}
|
||||
elsif($game_port_listening)
|
||||
{
|
||||
$status = "ONLINE";
|
||||
$ready = 1;
|
||||
$last_error = "Game port is listening but the managed screen session is not running.";
|
||||
}
|
||||
else
|
||||
{
|
||||
$status = "OFFLINE";
|
||||
$status_state = "Stopped";
|
||||
}
|
||||
|
||||
return {
|
||||
status => $status,
|
||||
StatusState => $status_state,
|
||||
status_state => $status_state,
|
||||
ready => $ready,
|
||||
ProcessRunning => $process_running,
|
||||
process_running => $process_running,
|
||||
session_running => $session_running,
|
||||
game_port_listening => $game_port_listening ? 1 : 0,
|
||||
query_port_listening => $query_port_listening ? 1 : 0,
|
||||
rcon_port_listening => $rcon_port_listening ? 1 : 0,
|
||||
PortValidationEnabled => PORT_VALIDATION_ENABLED ? 1 : 0,
|
||||
StartupValidationTimeoutSeconds => $startup_timeout,
|
||||
PortCheckIntervalSeconds => PORT_CHECK_INTERVAL_SECONDS,
|
||||
ExpectedPorts => \@expected_ports,
|
||||
expected_ports => \@expected_ports,
|
||||
ListeningPorts => $listening_ports,
|
||||
listening_ports => $listening_ports,
|
||||
MissingPorts => $missing_ports,
|
||||
missing_ports => $missing_ports,
|
||||
CPUUsage => get_agent_cpu_usage_percent(),
|
||||
MemoryUsage => get_agent_memory_usage_percent(),
|
||||
pid => $pid,
|
||||
session_name => $screen_id,
|
||||
ip => $server_ip,
|
||||
|
|
|
|||
89
OGP64/OGP/tests/port_validation_smoke.sh
Executable file
89
OGP64/OGP/tests/port_validation_smoke.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: $0 <port[/tcp|/udp|/any]> [more ports...]"
|
||||
echo "Example: $0 2302/udp 2303/udp 27015/tcp"
|
||||
exit 64
|
||||
fi
|
||||
|
||||
declare -A expected_tcp=()
|
||||
declare -A expected_udp=()
|
||||
declare -A listening_tcp=()
|
||||
declare -A listening_udp=()
|
||||
|
||||
add_expected() {
|
||||
local value="$1"
|
||||
local port="${value%%/*}"
|
||||
local proto="any"
|
||||
if [[ "$value" == */* ]]; then
|
||||
proto="${value##*/}"
|
||||
fi
|
||||
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
||||
echo "Invalid port: $value"
|
||||
exit 65
|
||||
fi
|
||||
case "$proto" in
|
||||
tcp) expected_tcp["$port"]=1 ;;
|
||||
udp) expected_udp["$port"]=1 ;;
|
||||
any) expected_tcp["$port"]=1; expected_udp["$port"]=1 ;;
|
||||
*) echo "Invalid protocol: $value"; exit 65 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for item in "$@"; do
|
||||
add_expected "$item"
|
||||
done
|
||||
|
||||
collect_with_powershell() {
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\$p=[System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties();\$p.GetActiveTcpListeners()|ForEach-Object{'TCP '+\$_.Port};\$p.GetActiveUdpListeners()|ForEach-Object{'UDP '+\$_.Port}" 2>/dev/null
|
||||
}
|
||||
|
||||
collect_with_netstat() {
|
||||
netstat -an 2>/dev/null | awk '
|
||||
toupper($1) == "TCP" && toupper($4) == "LISTENING" { split($2,a,/[:.]/); print "TCP " a[length(a)] }
|
||||
toupper($1) == "UDP" { split($2,a,/[:.]/); print "UDP " a[length(a)] }
|
||||
'
|
||||
}
|
||||
|
||||
while read -r proto port; do
|
||||
[ -n "${proto:-}" ] || continue
|
||||
case "$proto" in
|
||||
TCP) listening_tcp["$port"]=1 ;;
|
||||
UDP) listening_udp["$port"]=1 ;;
|
||||
esac
|
||||
done < <(collect_with_powershell || collect_with_netstat)
|
||||
|
||||
missing=0
|
||||
found=0
|
||||
|
||||
echo "Expected ports:"
|
||||
for port in "${!expected_tcp[@]}"; do
|
||||
if [ "${listening_tcp[$port]+x}" ]; then
|
||||
echo " TCP $port listening"
|
||||
found=$((found + 1))
|
||||
else
|
||||
echo " TCP $port missing"
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
for port in "${!expected_udp[@]}"; do
|
||||
if [ "${listening_udp[$port]+x}" ]; then
|
||||
echo " UDP $port listening"
|
||||
found=$((found + 1))
|
||||
else
|
||||
echo " UDP $port missing"
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$missing" -eq 0 ]; then
|
||||
echo "Result: Running"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$found" -gt 0 ]; then
|
||||
echo "Result: Warning"
|
||||
exit 2
|
||||
fi
|
||||
echo "Result: Failed"
|
||||
exit 3
|
||||
Loading…
Add table
Add a link
Reference in a new issue