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

View file

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

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

View file

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

View file

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