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
|
||||
|
|
@ -31,6 +31,7 @@ The Windows agent mirrors the Linux agent as closely as practical while using Cy
|
|||
- [`docs/AGENT_ARCHITECTURE.md`](docs/AGENT_ARCHITECTURE.md)
|
||||
- [`docs/CYGWIN_INTEGRATION.md`](docs/CYGWIN_INTEGRATION.md)
|
||||
- [`docs/COMMAND_EXECUTION.md`](docs/COMMAND_EXECUTION.md)
|
||||
- [`docs/GSP_WINDOWS_AGENT_PORT_VALIDATION.md`](docs/GSP_WINDOWS_AGENT_PORT_VALIDATION.md)
|
||||
- [`docs/PROCESS_MANAGEMENT.md`](docs/PROCESS_MANAGEMENT.md)
|
||||
- [`docs/PANEL_INTEGRATION.md`](docs/PANEL_INTEGRATION.md)
|
||||
|
||||
|
|
|
|||
202
docs/GSP_WINDOWS_AGENT_PORT_VALIDATION.md
Normal file
202
docs/GSP_WINDOWS_AGENT_PORT_VALIDATION.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# GSP Windows Agent Port Validation
|
||||
|
||||
Workspace reference: [`GSP-WORKSPACE.md`](../../GSP-WORKSPACE.md)
|
||||
|
||||
## Current Behavior
|
||||
|
||||
The Windows agent is a Cygwin-packaged OGP Perl agent. The maintained runtime lives under `OGP64/`, and the core service is `OGP64/OGP/ogp_agent.pl`.
|
||||
|
||||
## Documentation Review Notes
|
||||
|
||||
Reviewed local project documentation:
|
||||
|
||||
- `README.md`
|
||||
- `docs/AGENT_ARCHITECTURE.md`
|
||||
- `docs/CYGWIN_INTEGRATION.md`
|
||||
- `docs/COMMAND_EXECUTION.md`
|
||||
- `docs/PROCESS_MANAGEMENT.md`
|
||||
- `docs/PANEL_INTEGRATION.md`
|
||||
- `OGP64/OGP/README.md`
|
||||
- `OGP64/OGP/documentation/agent-guide.md`
|
||||
- related GSP Panel docs under `../GSP/docs/architecture/` and `../GSP/docs/features/STATUS_SYSTEM.md`
|
||||
|
||||
Documentation not found in this repository:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `CODEX_PROJECT_GUIDE.md`
|
||||
- a dedicated protocol/API document before this feature note
|
||||
- a dedicated networking/status validation document before this feature note
|
||||
|
||||
## Architecture Discovery
|
||||
|
||||
Windows startup flow:
|
||||
|
||||
1. `OGP64/agent_start.bat` sets the Cygwin path and validates `OGP64/OGP/ogp_agent.pl`.
|
||||
2. The batch script creates missing config files from `.default` templates.
|
||||
3. It runs `perl -c ./ogp_agent.pl`.
|
||||
4. It launches the agent from `/OGP`.
|
||||
|
||||
Shutdown flow:
|
||||
|
||||
1. `OGP64/agent_stop.bat` reads known PID files.
|
||||
2. It sends termination signals through the bundled Cygwin `kill.exe`.
|
||||
|
||||
Communication with Panel:
|
||||
|
||||
- XML-RPC over `/RPC2`
|
||||
- shared key configured in `OGP64/OGP/Cfg/Config.pm`
|
||||
- command dispatch table in `OGP64/OGP/ogp_agent.pl`
|
||||
- structured status command: `server_status`
|
||||
|
||||
Server launch process:
|
||||
|
||||
- Panel calls `universal_start`.
|
||||
- Agent writes a startup hint under `/OGP/startups`.
|
||||
- Agent launches the server inside a managed screen session.
|
||||
- The game command receives Panel-generated startup parameters, including the assigned game port.
|
||||
|
||||
Server stop process:
|
||||
|
||||
- Panel calls `stop_server`.
|
||||
- Agent writes a `STOPPING` status hint.
|
||||
- Agent removes the startup flag and kills the managed process tree.
|
||||
|
||||
Server status process:
|
||||
|
||||
- Panel calls `server_status` when available.
|
||||
- Agent checks the managed screen session.
|
||||
- Agent validates Panel-provided ports.
|
||||
- Agent returns a compatibility `status` plus richer port fields.
|
||||
|
||||
Before this change, the structured `server_status` RPC used these inputs:
|
||||
|
||||
- `home_id`
|
||||
- `server_ip`
|
||||
- `server_port`
|
||||
- `query_port`
|
||||
- `rcon_port`
|
||||
- `startup_timeout`
|
||||
- `state_hint`
|
||||
|
||||
The agent checked the managed GNU Screen session and probed ports with `netstat`. It primarily treated the game port as the readiness indicator and returned compatibility fields such as `status`, `ready`, `process_running`, `session_running`, `game_port_listening`, `query_port_listening`, and `rcon_port_listening`.
|
||||
|
||||
This was better than checking only a process, but it still had gaps:
|
||||
|
||||
- it did not expose a complete expected/listening/missing port list
|
||||
- it only modeled game/query/RCON ports as individual checks
|
||||
- it did not return the requested `Stopped`, `Starting`, `Running`, `Warning`, and `Failed` state model
|
||||
- it preferred `netstat`, while Windows can expose listening ports through .NET networking APIs
|
||||
|
||||
## Proposed Behavior
|
||||
|
||||
The agent should validate only the ports assigned by the Panel for the specific server.
|
||||
|
||||
The Panel remains the source of truth for expected ports. The agent must not scan random application ports, guess ports, or hardcode game-specific port rules.
|
||||
|
||||
The status result now keeps the existing compatibility fields and adds richer fields:
|
||||
|
||||
- `ProcessRunning`
|
||||
- `StatusState`
|
||||
- `ExpectedPorts`
|
||||
- `ListeningPorts`
|
||||
- `MissingPorts`
|
||||
- `CPUUsage`
|
||||
- `MemoryUsage`
|
||||
- `PortValidationEnabled`
|
||||
- `StartupValidationTimeoutSeconds`
|
||||
- `PortCheckIntervalSeconds`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```text
|
||||
Panel Game Monitor
|
||||
-> Panel/includes/lib_remote.php remote_server_status()
|
||||
-> XML-RPC server_status
|
||||
-> OGP64/OGP/ogp_agent.pl
|
||||
-> managed screen/session check
|
||||
-> configured port validation
|
||||
-> structured status hash
|
||||
-> Panel display logic
|
||||
```
|
||||
|
||||
## Status Flow
|
||||
|
||||
1. Panel calls `server_status` with the server's assigned game/query/RCON ports.
|
||||
2. Agent checks the managed screen session for `home_id`.
|
||||
3. Agent builds `ExpectedPorts` from the Panel-provided ports.
|
||||
4. Agent collects listening ports using PowerShell/.NET `System.Net.NetworkInformation.IPGlobalProperties` when available.
|
||||
5. Agent falls back to `netstat -an` if PowerShell/.NET collection fails.
|
||||
6. Agent compares expected ports with active TCP/UDP listeners.
|
||||
7. Agent returns old compatibility fields and new detailed fields.
|
||||
|
||||
## State Model
|
||||
|
||||
| `StatusState` | Meaning | Compatibility `status` |
|
||||
| --- | --- | --- |
|
||||
| `Stopped` | No managed process/session and no configured port evidence. | `OFFLINE` |
|
||||
| `Starting` | Process/session exists, but required ports are not listening yet. | `STARTING` |
|
||||
| `Running` | Process/session exists and all expected ports are listening. | `ONLINE` |
|
||||
| `Warning` | Process/session exists and only some expected ports are listening, or ports listen without the managed session. | `ONLINE` |
|
||||
| `Failed` | Process/session exists and no expected ports are listening after timeout. | `UNRESPONSIVE` |
|
||||
|
||||
The compatibility `status` field is intentionally preserved so existing Panel code does not break.
|
||||
|
||||
## Panel Integration
|
||||
|
||||
Current Panel integration is already compatible:
|
||||
|
||||
- `Panel/includes/lib_remote.php::remote_server_status()`
|
||||
- `Panel/modules/gamemanager/home_handling_functions.php::get_agent_server_status()`
|
||||
|
||||
The Panel currently passes the assigned game port plus derived query and RCON ports. Future Panel work can pass multiple ports in the existing `query_port` or `rcon_port` strings using comma, semicolon, or whitespace-separated values, with optional protocol markers such as `2302/udp` or `tcp:27015`.
|
||||
|
||||
## Agent Integration
|
||||
|
||||
Changed agent file:
|
||||
|
||||
- `OGP64/OGP/ogp_agent.pl`
|
||||
|
||||
Changed default config file:
|
||||
|
||||
- `OGP64/OGP/Cfg/Preferences.pm.default`
|
||||
|
||||
The agent does not introduce a new RPC. It extends the existing `server_status` response.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Add these keys to `Cfg/Preferences.pm`:
|
||||
|
||||
| Key | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `PortValidationEnabled` | `1` | Enables configured port validation. |
|
||||
| `StartupValidationTimeoutSeconds` | `180` | Time before a process with no listening required ports is treated as failed. |
|
||||
| `PortCheckIntervalSeconds` | `5` | Polling interval for future startup wait loops. Current RPC checks once per call. |
|
||||
|
||||
## Testing Plan
|
||||
|
||||
Validation scenarios:
|
||||
|
||||
| Scenario | Expected result |
|
||||
| --- | --- |
|
||||
| Process/session running and all expected ports listening | `StatusState=Running`, `status=ONLINE` |
|
||||
| Process/session running and some expected ports listening | `StatusState=Warning`, `status=ONLINE`, missing ports listed |
|
||||
| Process/session running and no ports before timeout | `StatusState=Starting`, `status=STARTING` |
|
||||
| Process/session running and no ports after timeout | `StatusState=Failed`, `status=UNRESPONSIVE` |
|
||||
| No process/session and no expected ports listening | `StatusState=Stopped`, `status=OFFLINE` |
|
||||
| No process/session but expected port is listening | `StatusState=Running` or `Warning`, `status=ONLINE`, warning message |
|
||||
|
||||
Manual Windows validation:
|
||||
|
||||
1. Start the agent with `C:\OGP64\agent_start.bat`.
|
||||
2. Start a test server from the Panel.
|
||||
3. Confirm `server_status` reports `Starting` until assigned ports bind.
|
||||
4. Confirm all assigned ports appear under `ExpectedPorts`.
|
||||
5. Confirm matching ports appear under `ListeningPorts`.
|
||||
6. Confirm unbound assigned ports appear under `MissingPorts`.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Add Panel-side support for passing a first-class array of expected ports instead of overloading optional port strings.
|
||||
- Add per-game startup timeout values from XML or Panel settings.
|
||||
- Add process-specific CPU and memory usage when the game server PID tree can be mapped reliably.
|
||||
- Add automated integration tests that call the XML-RPC endpoint on a Windows/Cygwin test host.
|
||||
|
|
@ -9,7 +9,16 @@ The Panel is authoritative. The Windows agent executes the work the Panel reques
|
|||
- shared key and RPC configuration live in `OGP64/OGP/Cfg/Config.pm`
|
||||
- startup preferences live in `OGP64/OGP/Cfg/bash_prefs.cfg`
|
||||
- the Panel talks to the same command surface as the Linux agent wherever practical
|
||||
- game server readiness is reported through the existing `server_status` RPC, extended with expected/listening/missing port details
|
||||
|
||||
## Compatibility rule
|
||||
|
||||
The Windows agent should mirror the Linux agent behaviorally as much as possible so the Panel can treat both platforms as one product family.
|
||||
|
||||
## Status validation
|
||||
|
||||
The Panel remains the source of truth for assigned ports. The Windows agent validates only the ports supplied by the Panel for a specific server.
|
||||
|
||||
Detailed design:
|
||||
|
||||
- [`GSP_WINDOWS_AGENT_PORT_VALIDATION.md`](GSP_WINDOWS_AGENT_PORT_VALIDATION.md)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ The Windows agent manages customer servers through the Cygwin runtime and the OG
|
|||
|
||||
- `OGP64` is the Cygwin root for the maintained launcher
|
||||
- process state is tracked through the agent runtime and PID files
|
||||
- configured port validation is handled by `server_status` in `OGP64/OGP/ogp_agent.pl`
|
||||
- manual startup and shutdown are handled by the root batch scripts
|
||||
- Windows-specific user and service assumptions belong here, not in the Panel
|
||||
|
||||
|
|
@ -21,3 +22,9 @@ The Windows agent manages customer servers through the Cygwin runtime and the OG
|
|||
## Rule
|
||||
|
||||
Keep startup and stop behavior visible and explicit. Failures should be reported in the same console when launched manually.
|
||||
|
||||
## Port validation
|
||||
|
||||
Detailed status validation design:
|
||||
|
||||
- [`GSP_WINDOWS_AGENT_PORT_VALIDATION.md`](GSP_WINDOWS_AGENT_PORT_VALIDATION.md)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue