refixed the server query

This commit is contained in:
Frank Harris 2026-06-08 13:06:32 -05:00
parent eedc3e8fb3
commit e2dd5a496c
8 changed files with 539 additions and 20 deletions

View file

@ -5,5 +5,7 @@
ftp_method => 'PureFTPd',
ogp_autorestart_server => '1',
protocol_shutdown_waittime => '10',
PortValidationEnabled => '1',
StartupValidationTimeoutSeconds => '180',
PortCheckIntervalSeconds => '5',
);

View file

@ -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.

View 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,

View 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