From b5dcf01a8c34478f1837255c6a8981904d1a29c2 Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Fri, 5 Jun 2026 11:07:47 -0500 Subject: [PATCH] Codex documentstion created --- AI_GSP_ARCHITECTURE.md | 66 + Agent-Windows/ogp_agent.pl | 711 +++++---- Agent_Linux/ogp_agent.pl | 1157 ++++++++------ COMPANION_PROGRAMS_DESIGN.md | 866 ++++++++++ GSP_PLATFORM_IMPROVEMENT_REPORT.md | 1020 ++++++++++++ Panel/includes/lib_remote.php | 40 +- .../gamemanager/home_handling_functions.php | 63 +- Panel/modules/gamemanager/log.php | 120 +- Panel/modules/gamemanager/restart_server.php | 88 +- Panel/modules/gamemanager/server_monitor.php | 19 +- Panel/modules/gamemanager/start_server.php | 80 +- Panel/modules/gamemanager/stop_server.php | 30 +- Panel/modules/gamemanager/view_server_log.php | 120 +- SCHEDULER_ACTIONS_DESIGN.md | 810 ++++++++++ STEAM_WORKSHOP_DESIGN.md | 1396 +++++++++++++++++ docs/agents/LINUX_AGENT.md | 109 ++ docs/agents/WINDOWS_AGENT.md | 94 ++ docs/architecture/PANEL_AGENT_FLOW.md | 103 ++ docs/architecture/REPOSITORY_OVERVIEW.md | 159 ++ docs/development/CODEX_GUIDE.md | 138 ++ docs/features/STATUS_SYSTEM.md | 54 + docs/features/WORKSHOP_SYSTEM.md | 53 + docs/features/XML_SYSTEM.md | 105 ++ docs/modules/GAMEMANAGER.md | 82 + docs/modules/MODULE_INDEX.md | 68 + docs/modules/SCHEDULER.md | 96 ++ docs/modules/SERVER_CONTENT_MANAGER.md | 70 + 27 files changed, 6648 insertions(+), 1069 deletions(-) create mode 100644 AI_GSP_ARCHITECTURE.md create mode 100644 COMPANION_PROGRAMS_DESIGN.md create mode 100644 GSP_PLATFORM_IMPROVEMENT_REPORT.md create mode 100644 SCHEDULER_ACTIONS_DESIGN.md create mode 100644 STEAM_WORKSHOP_DESIGN.md create mode 100644 docs/agents/LINUX_AGENT.md create mode 100644 docs/agents/WINDOWS_AGENT.md create mode 100644 docs/architecture/PANEL_AGENT_FLOW.md create mode 100644 docs/architecture/REPOSITORY_OVERVIEW.md create mode 100644 docs/development/CODEX_GUIDE.md create mode 100644 docs/features/STATUS_SYSTEM.md create mode 100644 docs/features/WORKSHOP_SYSTEM.md create mode 100644 docs/features/XML_SYSTEM.md create mode 100644 docs/modules/GAMEMANAGER.md create mode 100644 docs/modules/MODULE_INDEX.md create mode 100644 docs/modules/SCHEDULER.md create mode 100644 docs/modules/SERVER_CONTENT_MANAGER.md diff --git a/AI_GSP_ARCHITECTURE.md b/AI_GSP_ARCHITECTURE.md new file mode 100644 index 00000000..d4175d45 --- /dev/null +++ b/AI_GSP_ARCHITECTURE.md @@ -0,0 +1,66 @@ +# GSP Component Architecture + +This repository contains the related GSP components together: + +- `/Agent-Windows` - Cygwin/Windows game server agent. +- `/Agent_Linux` - Linux game server agent. +- `/Panel` - PHP control panel used by customers and staff. +- `/Website` - public website and related customer-facing pages. + +## Panel And Agent Communication + +The Panel talks to agents through XML-RPC using the encrypted parameter helpers in `Panel/includes/lib_remote.php`. The Panel should request actions from the agent and treat the agent as the source of truth for server state. + +The shared server control backend is still `screen` for both Linux and Cygwin/Windows agents. The Panel should not decide that a game server is online only because a web request, LGSL query, or GameQ query succeeded. + +## Status Source Of Truth + +The agent is the source of truth for start, stop, restart, and status. Marker files such as `SERVER_STOPPED` or startup flag files may still exist for legacy autorestart and startup bookkeeping, but they are not trusted as the current server state. + +The minimum readiness check is: + +1. The configured process/session exists. +2. The configured game port is listening. + +LGSL/GameQ query data is optional metadata only. It can provide player counts, maps, hostname, and other query details. If query fails while the process/session exists and the game port is listening, the Panel should show the server as online with a small "query info unavailable" note. + +## Status Model + +- `OFFLINE`: no managed process/session and no listening game port. +- `STARTING`: managed process/session exists, but the game port is not listening yet. +- `ONLINE`: managed process/session exists and the game port is listening. +- `STOPPING`: stop was requested and the managed process/session still exists. +- `UNRESPONSIVE`: the managed process/session exists but the game port did not become ready before timeout, or stop did not complete cleanly. +- `UNKNOWN`: the agent cannot be reached or did not return structured status. + +## Start + +Start should launch the configured screen/session and immediately report a starting state if the session exists. Slow games, including Arma 2 and DayZ Mod, must not be marked failed just because LGSL/GameQ did not answer quickly. + +Startup timeout should be configurable per game XML when possible, with a default of 180 seconds. + +## Stop + +Stop should issue the configured graceful control command when available. The agent then checks whether the managed screen/session is gone and whether the game port is no longer listening. If the process/session remains after the wait period, the agent escalates by closing the screen session. + +The Panel should only show stopped after the agent confirms `OFFLINE`. + +## Restart + +Restart is intentionally simple: + +1. Stop server. +2. Wait 60 seconds. +3. Start server. + +Do not use a separate shortcut that skips stop verification or depends on query responses. + +## Log Viewer + +The gamemanager log viewer should update the log text through AJAX/live fetches only. It should not reload the full page for each refresh and should not show the old manual refresh interval dropdown or plus/update button. + +The live log should preserve mobile scrolling behavior and only auto-scroll when the user is already near the bottom. + +## Agent Parity + +Keep Linux and Cygwin/Windows agent behavior as similar as possible. Both should continue using `screen` as the shared backend for now and expose the same status model to the Panel. diff --git a/Agent-Windows/ogp_agent.pl b/Agent-Windows/ogp_agent.pl index bc290caa..72f2ac7f 100644 --- a/Agent-Windows/ogp_agent.pl +++ b/Agent-Windows/ogp_agent.pl @@ -238,7 +238,7 @@ elsif ($no_startups != 1) $run_dir, $startup_cmd, $server_port, $server_ip, $cpu, $nice, $preStart, $envVars, $game_key, $console_log ) = split(',', $_); - + if (is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1) { @@ -272,7 +272,7 @@ logger "Open Game Panel - Agent started - " . " - PID $$", 1; # Stop previous scheduler process if exists -scheduler_stop(); +scheduler_stop(); # Create new object with default dispatcher for scheduled tasks my $cron = new Schedule::Cron( \&scheduler_dispatcher, { nofork => 1, @@ -295,8 +295,9 @@ if(-e Path::Class::File->new(FD_DIR, 'Settings.pm')) my $d = Frontier::Daemon::OGP::Forking->new( methods => { - is_screen_running => \&is_screen_running, - universal_start => \&universal_start, + is_screen_running => \&is_screen_running, + server_status => \&server_status, + universal_start => \&universal_start, cpu_count => \&cpu_count, rfile_exists => \&rfile_exists, quick_chk => \&quick_chk, @@ -360,9 +361,9 @@ my $d = Frontier::Daemon::OGP::Forking->new( sub backup_home_log { my ($home_id, $log_file, $console_log_file) = @_; - + my $home_backup_dir = SCREEN_LOGS_DIR . "/home_id_" . $home_id; - + if( ! -e $home_backup_dir ) { if( ! mkdir $home_backup_dir ) @@ -371,13 +372,13 @@ sub backup_home_log return 1; } } - + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - + my $backup_file_name = $mday . $mon . $year . '_' . $hour . 'h' . $min . 'm' . $sec . "s.log"; - + my $output_path = $home_backup_dir . "/" . $backup_file_name; - + # Used for deleting log files older than DELETE_LOGS_AFTER my @file_list; my @find_dirs; # directories to search @@ -391,37 +392,37 @@ sub backup_home_log my $seconds_per_day = 60*60*24; # seconds in a day my $AGE = $days*$seconds_per_day; # age in seconds push (@find_dirs, $home_backup_dir); - - # Create local copy of log file backup in the log_backups folder and current user home directory if SCREEN_LOG_LOCAL = 1 + + # Create local copy of log file backup in the log_backups folder and current user home directory if SCREEN_LOG_LOCAL = 1 if(SCREEN_LOG_LOCAL == 1) { # Create local backups folder my $local_log_folder = Path::Class::Dir->new("logs_backup"); - + if(!-e $local_log_folder){ mkdir($local_log_folder); } - + # Add full path to @find_dirs so that log files older than DELETE_LOGS_AFTER are deleted my $fullpath_to_local_logs = Path::Class::Dir->new(getcwd(), "logs_backup"); push (@find_dirs, $fullpath_to_local_logs); - + my $log_local = $local_log_folder . "/" . $backup_file_name; - + # Delete the local log file if it already exists if(-e $log_local){ unlink $log_local; } - + # If the log file contains UPDATE in the filename, do not allow users to see it since it will contain steam credentials # Will return -1 for not existing my $isUpdate = index($log_file,SCREEN_TYPE_UPDATE); - + if($isUpdate == -1){ copy($log_file,$log_local); } } - + # Delete all files in @find_dirs older than DELETE_LOGS_AFTER days find ( sub { my $file = $File::Find::name; @@ -429,14 +430,14 @@ sub backup_home_log push (@file_list, $file); } }, @find_dirs); - + # Include the custom console path - and also do a size check on it if(defined $console_log_file && $console_log_file ne ""){ my $path_to_console_file = $console_log_file; if( -f $path_to_console_file){ push (@file_list, $path_to_console_file); - - # Backup and delete this specific file as well if it's over 20MB + + # Backup and delete this specific file as well if it's over 20MB my @stats = stat($path_to_console_file); if($stats[7] >= 20971520){ if(SCREEN_LOG_LOCAL == 1){ @@ -450,7 +451,7 @@ sub backup_home_log } } } - + for my $file (@file_list) { if( -f $file ){ my @stats = stat($file); @@ -459,9 +460,9 @@ sub backup_home_log } } } - + move($log_file,$output_path); - + return 0; } @@ -485,16 +486,16 @@ sub create_screen_cmd_loop { my ($screen_id, $exec_cmd, $priority, $affinity, $envVars) = @_; my $server_start_batfile = "_serverStart.bat"; - + $exec_cmd = replace_OGP_Env_Vars($screen_id, "", "", $exec_cmd); - + # Create batch file that will launch the process and store PID which will be used for killing later open (SERV_START_BAT_SCRIPT, '>', $server_start_batfile); - + my $batch_server_command = "\@echo off" . "\r\n" . "setlocal EnableDelayedExpansion" . "\r\n" . ":TOP" . "\r\n"; - + if(defined $envVars && $envVars ne ""){ $batch_server_command .= $envVars; }# lines 500-515, inside sub create_screen_cmd_loop @@ -514,12 +515,12 @@ $batch_server_command .= "set STARTTIME=%TIME: =0%" . "\r\n" . "IF \"%hour%\" == \"00\" IF \"%minute%\" == \"00\" IF %second% lss 60 exit\r\n" . "timeout /t 30 /nobreak >nul\r\n" . "goto TOP\r\n"; - + print SERV_START_BAT_SCRIPT $batch_server_command; close (SERV_START_BAT_SCRIPT); - + my $screen_exec_script = "cmd /Q /C " . $server_start_batfile; - + return sprintf('screen -d -m -t "%1$s" -c ' . SCREENRC_FILE . ' -S %1$s %2$s', $screen_id, $screen_exec_script); @@ -529,17 +530,17 @@ $batch_server_command .= "set STARTTIME=%TIME: =0%" . "\r\n" sub replace_OGP_Env_Vars{ # This function replaces constants from environment variables set in the XML my ($screen_id, $homeid, $homepath, $exec_cmd, $game_key) = @_; - + # Handle steam specific replacements if(defined $screen_id && $screen_id ne ""){ my $screen_id_for_txt_update = substr ($screen_id, rindex($screen_id, '_') + 1); my $steamInsFile = $screen_id_for_txt_update . "_install.txt"; my $steamCMDPath = STEAMCMD_CLIENT_DIR; my $fullPath = Path::Class::File->new($steamCMDPath, $steamInsFile); - + my $windows_steamCMDPath= clean(`cygpath -wa $steamCMDPath`); $windows_steamCMDPath =~ s#/#\\#g; - + # If the install file exists, the game can be auto updated, else it will be ignored by the game for improper syntax # To generate the install file, the "Install/Update via Steam" button must be clicked on at least once! if(-e $fullPath){ @@ -552,13 +553,13 @@ sub replace_OGP_Env_Vars{ if(defined $homepath && $homepath ne ""){ $exec_cmd =~ s/{OGP_HOME_DIR}/$homepath/g; } - + # Handle windows directory replacement if(defined $homepath && $homepath ne ""){ my $windows_home_path = clean(`cygpath -wa $homepath`); $exec_cmd =~ s/{OGP_HOME_DIR_WINDOWS}/$windows_home_path/g; } - + # Handle global game shared directory replacement if(defined $game_key && $game_key ne ""){ my $readable_game_key = lc(substr($game_key, 0, rindex($game_key,"_"))); @@ -568,10 +569,10 @@ sub replace_OGP_Env_Vars{ { logger "Could not create " . $shared_path . " directory $!.", 1; } - + $exec_cmd =~ s/{OGP_GAME_SHARED_DIR}/$shared_path/g; } - + return $exec_cmd; } @@ -636,11 +637,11 @@ sub check_steam_cmd_client "http://media.steampowered.com/installer/" . $steam_client_file; logger "Downloading the Steam client from $steam_client_url to '" . $steam_client_path . "'."; - + my $ua = LWP::UserAgent->new; $ua->agent('Mozilla/5.0'); my $response = $ua->get($steam_client_url, ':content_file' => "$steam_client_path"); - + unless ($response->is_success) { logger "Failed to download steam installer from " @@ -695,6 +696,134 @@ sub is_screen_running_without_decrypt } } +sub server_status +{ + return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); + return server_status_without_decrypt(decrypt_params(@_)); +} + +sub get_status_hint_path +{ + my ($home_id) = @_; + $home_id =~ s/[^0-9]//g; + return Path::Class::File->new(GAME_STARTUP_DIR, "status-$home_id"); +} + +sub write_status_hint +{ + my ($home_id, $state) = @_; + my $hint_file = get_status_hint_path($home_id); + if(open(STATUSHINT, '>', $hint_file)) + { + print STATUSHINT time() . "," . $state; + close(STATUSHINT); + } +} + +sub read_status_hint +{ + my ($home_id) = @_; + my $hint_file = get_status_hint_path($home_id); + return (0, "") unless(-e $hint_file); + open(STATUSHINT, '<', $hint_file) or return (0, ""); + my $line = ; + close(STATUSHINT); + chomp($line); + my ($timestamp, $state) = split(/,/, $line, 2); + $timestamp = 0 unless(defined($timestamp) && $timestamp =~ /^\d+$/); + $state = "" unless(defined($state)); + return ($timestamp, $state); +} + +sub get_screen_pid_without_decrypt +{ + my ($home_id) = @_; + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); + my $screen_line = `screen -list | grep $screen_id`; + return $1 if($screen_line =~ /([0-9]+)\.$screen_id/); + return ""; +} + +sub is_port_listening_without_decrypt +{ + my ($server_ip, $port) = @_; + 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 =~ /[:.]$port\s+.*(LISTENING|UDP)/i); + return 0; +} + +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]+$/); + $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 ($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 $ready = 0; + + if($session_running && $game_port_listening) + { + $status = "ONLINE"; + $ready = 1; + } + elsif($session_running) + { + if($effective_hint eq "STOPPING") + { + $status = "STOPPING"; + } + 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."; + } + else + { + $status = "STARTING"; + } + } + elsif($game_port_listening) + { + $status = "UNRESPONSIVE"; + $last_error = "Game port is listening but the managed screen session is not running."; + } + else + { + $status = "OFFLINE"; + } + + return { + status => $status, + ready => $ready, + 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, + pid => $pid, + session_name => $screen_id, + ip => $server_ip, + port => $server_port, + query_port => $query_port, + rcon_port => $rcon_port, + last_error => $last_error, + query_info => "" + }; +} + # Delete Server Stopped Status File: sub deleteStoppedStatFile { @@ -721,7 +850,7 @@ sub universal_start_without_decrypt $home_id, $home_path, $server_exe, $run_dir, $startup_cmd, $server_port, $server_ip, $cpu, $nice, $preStart, $envVars, $game_key, $console_log ) = @_; - + if (is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1) { logger "This server is already running (ID: $home_id)."; @@ -750,18 +879,18 @@ sub universal_start_without_decrypt return -13; } } - + if(defined $preStart && $preStart ne ""){ # Get it in the format that the startup file can use $preStart = startup_comma_format_to_multiline($preStart); }else{ $preStart = ""; } - + if(defined $envVars && $envVars ne ""){ # Get it in the format that the startup file can use - $envVars = startup_comma_format_to_multiline($envVars); - + $envVars = startup_comma_format_to_multiline($envVars); + # Replace variables in the envvars if they exist my @prestartenvvars = split /[\r\n]+/, $envVars; my $envVarStr = ""; @@ -772,7 +901,7 @@ sub universal_start_without_decrypt $envVarStr .= "$line\r\n"; } } - + if(defined $envVarStr && $envVarStr ne ""){ $envVars = $envVarStr; } @@ -781,7 +910,7 @@ sub universal_start_without_decrypt } my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); - + # Replace any OGP variables $startup_cmd = replace_OGP_Env_Vars($screen_id, $home_id, $home_path, $startup_cmd, $game_key); @@ -789,13 +918,13 @@ sub universal_start_without_decrypt my $priority; my $affinity; my $run_before_start; - + if($nice ne "NA") { if( $nice <= 19 and $nice >= 11 ) { $priority = "/low"; - } + } elsif( $nice <= 10 and $nice >= 1 ) { $priority = "/belownormal"; @@ -821,28 +950,28 @@ sub universal_start_without_decrypt { $priority = ""; } - + if($cpu ne "NA" and $cpu ne "" ) { - + $affinity = "/affinity $cpu"; } else { $affinity = ""; } - + my $win_game_binary_dir = clean(`cygpath -wa $game_binary_dir`); chomp $win_game_binary_dir; - + # Create the startup string. my ($file_extension) = $server_exe =~ /(\.[^.]+)$/; - + my $cli_bin; - + # Create bash file to respawn process if it crashes or exits without user interaction # If a user stops the server, the process will not respawn - + if($file_extension eq ".jar") { if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ @@ -856,7 +985,7 @@ sub universal_start_without_decrypt { # There is no software made for windows that uses bash by default, # but it can be a good way to improve the server startup. To be able to use - # sh/bash scripts as server executable I added this piece to the agent: + # sh/bash scripts as server executable I added this piece to the agent: if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ deleteStoppedStatFile($home_path); $cli_bin = create_screen_cmd_loop($screen_id, "bash $game_binary_dir/$server_exe $startup_cmd", $priority, $affinity); @@ -873,28 +1002,29 @@ sub universal_start_without_decrypt # Below lines should only be used for launching non-auto restart game servers $win_game_binary_dir =~ s/\\/\\\\/g; $startup_cmd =~ s/\\/\\\\/g; # Needs to be done on the startup_cmd as well in case it includes paths - + $cli_bin = create_screen_cmd($screen_id, "cmd /Q /C start $priority $affinity /WAIT $win_game_binary_dir\\\\$server_exe $startup_cmd"); } } - + $home_path =~ s/\\/\//g; - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log( $home_id, $log_file, $home_path . "/" . $console_log ); - + my $clean_cli_bin = $cli_bin; $clean_cli_bin =~ s/\\\\/\\/g; logger "Startup command [ $clean_cli_bin ] will be executed in dir $game_binary_dir."; - # Fix permissions + # Fix permissions my $ownerShipResults = take_ownership($home_path); # Run before start script and set environment variables which will affect create_screen_cmd only... loop already has the envvars as well $run_before_start = run_before_start_commands($home_id, $home_path, $preStart, $envVars); system($cli_bin); + write_status_hint($home_id, "STARTING"); if(defined $preStart && $preStart ne ""){ # Get it in the format that the startup file can use @@ -902,8 +1032,8 @@ sub universal_start_without_decrypt }else{ $preStart = ""; } - - if(defined $envVars && $envVars ne ""){ + + if(defined $envVars && $envVars ne ""){ # Get it in the format that the startup file can use $envVars = multiline_to_startup_comma_format($envVars); }else{ @@ -1024,9 +1154,9 @@ sub get_log { $log_file = Path::Class::File->new($home_path, $log_file); } - - chmod 0644, $log_file; - + + chmod 0644, $log_file; + # Create local copy of current log file if SCREEN_LOG_LOCAL = 1 if(SCREEN_LOG_LOCAL == 1) { @@ -1035,13 +1165,13 @@ sub get_log { unlink $log_local; } - + # Copy log file only if it's not an UPDATE type as it may contain steam credentials if($screen_type eq SCREEN_TYPE_HOME){ copy($log_file, $log_local); } } - + # Regenerate the log file if it doesn't exist unless ( -e $log_file ) { @@ -1057,12 +1187,12 @@ sub get_log return -8; } } - + # Return a few lines of output to the web browser my(@modedlines) = `tail -n $nb_of_lines $log_file`; - + my $linecount = 0; - + foreach my $line (@modedlines) { #Remove unwanted characters (https://superuser.com/questions/99128/removing-the-escape-characters-from-gnu-screens-screenlog-n) $line =~ s/\x1b[[()=][;?0-9]*[0-9A-Za-z]?//g; @@ -1081,8 +1211,8 @@ sub get_log $line =~ s/�//g; $modedlines[$linecount]=$line; $linecount++; - } - + } + my $encoded_content = encode_list(@modedlines); chdir AGENT_RUN_DIR; if(is_screen_running_without_decrypt($screen_type, $home_id) == 1) @@ -1108,24 +1238,25 @@ sub stop_server ### Return 0 on success sub stop_server_without_decrypt { - my ($home_id, $server_ip, $server_port, $control_protocol, - $control_password, $control_type, $home_path) = @_; + my ($home_id, $server_ip, $server_port, $control_protocol, + $control_password, $control_type, $home_path) = @_; + write_status_hint($home_id, "STOPPING"); # Clean up dead screen sessions before stopping system("screen -wipe"); - + my $startup_file = Path::Class::File->new(GAME_STARTUP_DIR, "$server_ip-$server_port"); - + if (-e $startup_file) { logger "Removing startup flag " . $startup_file . ""; unlink($startup_file) or logger "Cannot remove the startup flag file $startup_file $!"; } - + # Create file indicator that the game server has been stopped if defined if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ - + # Get current directory and chdir into the game's home dir my $curDir = getcwd(); chdir $home_path; @@ -1133,20 +1264,20 @@ sub stop_server_without_decrypt # Create stopped indicator file used by autorestart of OGP if server crashes open(STOPFILE, '>', "SERVER_STOPPED"); close(STOPFILE); - + # Return to original directory chdir $curDir; } - + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); my $get_screen_pid = "screen -list | grep $screen_id | cut -f1 -d'.' | sed '".'s/\W//g'."' | head -1"; - my $screen_pid = `$get_screen_pid`; - + my $screen_pid = `$get_screen_pid`; + chomp $screen_pid; - + my $windows_pid_command = "ps -W | grep '" . $screen_pid . "' | head -1 | awk '{print \$4}'"; my $windows_pid = `$windows_pid_command`; - + chomp $windows_pid; # Immediately kill the process @@ -1154,11 +1285,35 @@ sub stop_server_without_decrypt system("cmd /C taskkill /f /fi 'PID eq $windows_pid' /T"); system("screen -wipe $screen_pid > /dev/null 2>&1"); logger "Server ID $screen_pid : $home_id Process killed."; - - return 0; + + return verify_server_stopped_without_decrypt($home_id, $server_ip, $server_port); + } + +sub verify_server_stopped_without_decrypt +{ + my ($home_id, $server_ip, $server_port) = @_; + for(my $i = 0; $i < 30; $i++) + { + my $session_running = is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1 ? 1 : 0; + my $port_listening = is_port_listening_without_decrypt($server_ip, $server_port); + return 0 if(!$session_running && !$port_listening); + sleep 2; + } + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); + system("screen -S $screen_id -X quit"); + system("screen -wipe > /dev/null 2>&1"); + sleep 2; + my $session_running = is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1 ? 1 : 0; + my $port_listening = is_port_listening_without_decrypt($server_ip, $server_port); + if($session_running || $port_listening) + { + logger "Server $server_ip:$server_port is still running or listening after stop escalation."; + return 1; + } + return 0; } -##### Send RCON command +##### Send RCON command ### Return 0 when error occurred on decryption. ### Return 1 on success sub send_rcon_command @@ -1166,7 +1321,7 @@ sub send_rcon_command return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($home_id, $server_ip, $server_port, $control_protocol, $control_password, $control_type, $rconCommand) = decrypt_params(@_); - + # legacy console if ($control_protocol eq "lcon") { @@ -1181,14 +1336,14 @@ sub send_rcon_command } return 0; } - + # Some validation checks for the variables. if ($server_ip =~ /^\s*$/ || $server_port < 0 || $server_port > 65535) { logger("Invalid IP:Port given $server_ip:$server_port."); return 0; } - + if ($control_password !~ /^\s*$/) { if ($control_protocol eq "rcon") @@ -1202,7 +1357,7 @@ sub send_rcon_command ); logger "Sending RCON command to $server_ip:$server_port: \n$rconCommand \n ."; - + my(@modedlines) = $rcon->execute($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1216,9 +1371,9 @@ sub send_rcon_command password => $control_password, timeout => 2 ); - + logger "Sending RCON command to $server_ip:$server_port: \n $rconCommand \n ."; - + my(@modedlines) = $rcon2->run($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1232,9 +1387,9 @@ sub send_rcon_command password => $control_password, timeout => 2 ); - + logger "Sending RCON command via ArmaBE module to $server_ip:$server_port: \n $rconCommand \n ."; - + my(@modedlines) = $armabe->run($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1280,32 +1435,32 @@ sub dirlistfm { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my $datadir = &decrypt_param(@_); - + logger "Asked for dirlist of $datadir directory."; - + if (!-d $datadir) { logger "ERROR - Directory [ $datadir ] not found!"; return -1; } - + if (!opendir(DIR, $datadir)) { logger "ERROR - Can't open $datadir: $!"; return -2; } - + my %dirfiles = (); - + my ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks ); - + my $count = 0; - + chdir($datadir); - + while (readdir(DIR)) { #skip the . and .. special dirs @@ -1317,7 +1472,7 @@ sub dirlistfm $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks ) = stat($_); - + if(defined $uid) { $uid = getpwuid($uid); @@ -1326,7 +1481,7 @@ sub dirlistfm { $uid = ''; } - + if(defined $gid) { $gid = getgrgid($gid); @@ -1336,7 +1491,7 @@ sub dirlistfm $gid = ''; } - #This if else logic determines what it is, File, Directory, other + #This if else logic determines what it is, File, Directory, other if (-T $_) { # print "File\n"; @@ -1373,13 +1528,13 @@ sub dirlistfm $count++; } closedir(DIR); - + if ($count eq 0) { logger "Empty directory $datadir."; return 1; } - + chdir AGENT_RUN_DIR; #Now we return it to the webpage, as array return {%dirfiles}; @@ -1399,7 +1554,7 @@ sub readfile close(BLANK); } } - + if (!open(USERFILE, '<', $userfile)) { logger "ERROR - Can't open file $userfile for reading."; @@ -1413,12 +1568,12 @@ sub readfile $wholefile .= encode_base64($buf); } close(USERFILE); - + if(!defined $wholefile) { return "1; "; } - + return "1;" . $wholefile; } @@ -1516,7 +1671,7 @@ sub start_file_download return -2; } } - + my $download_file_path = Path::Class::File->new($destination, "$filename"); my $pid = fork(); @@ -1533,11 +1688,11 @@ sub start_file_download SSL_verify_mode => 0x00 } ); $ua->agent('Mozilla/5.0'); my $response = $ua->get($url, ':content_file' => "$download_file_path"); - + if ($response->is_success) { logger "Successfully fetched $url and stored it to $download_file_path. Retval: ".$response->status_line; - + if (!-e $download_file_path) { logger "File $download_file_path does not exist."; @@ -1593,13 +1748,13 @@ sub run_before_start_commands { #return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($server_id, $homedir, $beforestartcmd, $envVars) = @_; - + if ($homedir ne "" && $server_id ne ""){ my $windows_home_path = clean(`cygpath -wa $homedir`); - + # Run any prestart scripts if (defined $beforestartcmd && $beforestartcmd ne "") - { + { logger "Running pre-start XML commands before starting server ID $server_id with a home directory of $homedir."; my @prestartcmdlines = split /[\r\n]+/, $beforestartcmd; my $prestartcmdfile = $windows_home_path . '\_prestart.bat'; @@ -1615,8 +1770,8 @@ sub run_before_start_commands system("cmd /Q /C start /wait \"$prestartcmdfile\""); system("cmd /Q /C del \"$prestartcmdfile\""); } - - + + # Set and export any environment variables for game server developers unwilling to properly learn Linux if (defined $envVars && $envVars ne ""){ my @prestartenvvars = split /[\r\n]+/, $envVars; @@ -1628,11 +1783,11 @@ sub run_before_start_commands } } } - + }else{ return -2; } - + return 1; } @@ -1653,7 +1808,7 @@ sub startup_comma_format_to_multiline{ sub check_b4_chdir { my ( $path ) = @_; - + if (!-e $path) { logger "$path does not exist yet. Trying to create it..."; @@ -1664,29 +1819,29 @@ sub check_b4_chdir { sudo_exec_without_decrypt('mkdir -p ' . $path); } - + if (!-e $path) { return -1; - } + } } - + if (!chdir $path) { logger "Unable to change dir to '$path'."; return -1; } - + return 0; } sub create_bash_scripts { my ( $home_path, $bash_scripts_path, $precmd, $postcmd, @installcmds ) = @_; - + $home_path =~ s/('+)/'\"$1\"'/g; $bash_scripts_path =~ s/('+)/'\"$1\"'/g; - + my @precmdlines = split /[\r\n]+/, $precmd; my $precmdfile = 'preinstall.sh'; open FILE, '>', $precmdfile; @@ -1696,7 +1851,7 @@ sub create_bash_scripts } close FILE; chmod 0755, $precmdfile; - + my @postcmdlines = split /[\r\n]+/, $postcmd; my $postcmdfile = 'postinstall.sh'; open FILE, '>', $postcmdfile; @@ -1710,7 +1865,7 @@ sub create_bash_scripts "rm -f runinstall.sh\n"; close FILE; chmod 0755, $postcmdfile; - + my $installfile = 'runinstall.sh'; open FILE, '>', $installfile; print FILE "#!/bin/bash\n". @@ -1725,7 +1880,7 @@ sub create_bash_scripts "./$postcmdfile\n"; close FILE; chmod 0755, $installfile; - + return $installfile; } @@ -1743,16 +1898,16 @@ sub start_rsync_install } my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path) != 0) { return 0; } - + # Rsync install require the rsync binary to exist in the system # to enable this functionality. my $rsync_binary = Path::Class::File->new("/usr/bin", "rsync"); - + if (!-f $rsync_binary) { logger "Failed to start rsync update from " @@ -1762,30 +1917,30 @@ sub start_rsync_install } my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); - + backup_home_log( $home_id, $log_file ); - + my $path = $home_path; $path =~ s/('+)/'\"$1\"'/g; - my @installcmds = ("/usr/bin/rsync --log-file='" . AGENT_RSYNC_GENERIC_LOG . "' --archive --compress --copy-links --update --verbose rsync://$url '$path'", + my @installcmds = ("/usr/bin/rsync --log-file='" . AGENT_RSYNC_GENERIC_LOG . "' --archive --compress --copy-links --update --verbose rsync://$url '$path'", "cd '$path'", - "find -iname \\\*.exe -exec chmod -f +x {} \\\;", + "find -iname \\\*.exe -exec chmod -f +x {} \\\;", "find -iname \\\*.bat -exec chmod -f +x {} \\\;"); - + # Fix permissions my $ownerShipResults = take_ownership($home_path, "str"); if(defined $ownerShipResults && $ownerShipResults ne ""){ $postcmd .= "\n" . $ownerShipResults; - } - + } + my $installfile = create_bash_scripts( $home_path, $bash_scripts_path, $precmd, $postcmd, @installcmds ); my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); logger "Running rsync update: /usr/bin/rsync --log-file='" . AGENT_RSYNC_GENERIC_LOG . "' --archive --compress --copy-links --update --verbose rsync://$url '$home_path'"; system($screen_cmd); - + chdir AGENT_RUN_DIR; return 1; } @@ -1798,36 +1953,36 @@ sub master_server_update { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($home_id,$home_path,$ms_home_id,$ms_home_path,$exec_folder_path,$exec_path,$precmd,$postcmd) = decrypt_params(@_); - + if ( check_b4_chdir($home_path) != 0) { return 0; } - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path) != 0) { return 0; } my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); - + backup_home_log( $home_id, $log_file ); - + my $my_home_path = $home_path; $my_home_path =~ s/('+)/'\"$1\"'/g; $ms_home_path =~ s/('+)/'\"$1\"'/g; - + my @installcmds = ("cp -vuRf '$ms_home_path'/* '$my_home_path'"); my $installfile = create_bash_scripts( $home_path, $bash_scripts_path, $precmd, $postcmd, @installcmds ); my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); logger "Running master server update from home ID $home_id to home ID $ms_home_id"; system($screen_cmd); - + chdir AGENT_RUN_DIR; return 1; } @@ -1845,43 +2000,43 @@ sub steam_cmd sub steam_cmd_without_decrypt { my ($home_id, $home_path, $mod, $modname, $betaname, $betapwd, $user, $pass, $guard, $exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $filesToLockUnlock, $arch_bits) = @_; - + # Creates home path if it doesn't exist if ( check_b4_chdir($home_path) != 0) { return 0; } - + # Changes into root steamcmd OGP directory if ( check_b4_chdir(STEAMCMD_CLIENT_DIR) != 0) { return 0; } - + my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); my $screen_id_for_txt_update = substr ($screen_id, rindex($screen_id, '_') + 1); my $steam_binary = Path::Class::File->new(STEAMCMD_CLIENT_DIR, "steamcmd.exe"); my $installSteamFile = $screen_id_for_txt_update . "_install.txt"; - + my $windows_home_path = clean(`cygpath -wa $home_path`); - + my $installtxt = Path::Class::File->new($installSteamFile); - + open FILE, '>', $installtxt; print FILE "\@ShutdownOnFailedCommand 1\n"; print FILE "\@NoPromptForPassword 1\n"; - + # Handle requested SteamCMD architecture if(defined $arch_bits && $arch_bits ne ""){ print FILE "\@sSteamCmdForcePlatformBitness " . $arch_bits . "\n"; } - + if(defined STEAM_DL_LIMIT && STEAM_DL_LIMIT ne "" && is_integer(STEAM_DL_LIMIT) && STEAM_DL_LIMIT > 0){ print FILE "set_download_throttle " . STEAM_DL_LIMIT . "\n"; } - + print FILE "force_install_dir \"$windows_home_path\"\n"; - + if($guard ne '') { print FILE "set_steam_guard_code $guard\n"; @@ -1894,7 +2049,7 @@ sub steam_cmd_without_decrypt { print FILE "login anonymous\n"; } - + if($modname ne "") { print FILE "app_set_config $mod mod $modname\n"; @@ -1913,31 +2068,31 @@ sub steam_cmd_without_decrypt { print FILE "app_update $mod\n"; } - + print FILE "exit\n"; close FILE; - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path) != 0) { return 0; } - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log( $home_id, $log_file ); - + my $postcmd_mod = $postcmd; - - # Fix permissions + + # Fix permissions my $ownerShipResults = take_ownership($home_path, "str"); if(defined $ownerShipResults && $ownerShipResults ne ""){ $postcmd_mod .= "\n" . $ownerShipResults; } - + my @installcmds = ("$steam_binary +runscript $installtxt +exit"); my $installfile = create_bash_scripts( $home_path, $bash_scripts_path, $precmd, $postcmd_mod, @installcmds ); - + my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); logger "Running steam update: $steam_binary +runscript $installtxt +exit"; system($screen_cmd); @@ -1950,26 +2105,26 @@ sub fetch_steam_version return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($appId, $pureOutput) = &decrypt_params(@_); my $ua = LWP::UserAgent->new; - + # Windows steam_cmd is bugged. So we cannot use the following until it's fixed. # https://github.com/ValveSoftware/Source-1-Games/issues/1929 #my $steam_binary = Path::Class::File->new(STEAMCMD_CLIENT_DIR, "steamcmd.exe"); #my $steam_options = "+login anonymous +app_info_update 1 +app_info_print \"$appId\" +quit"; #my $grep = $pureOutput != "0" ? "" : '| grep -EA 1000 "^\s+\"branches\"$" | grep -EA 5 "^\s+\"public\"$" | grep -m 1 -EB 10 "^\s+}$" | grep -E "^\s+\"buildid\"\s+" | tr \'[:blank:]"\' \' \' | tr -s \' \' | cut -d\' \' -f3'; - + #my $response = `$steam_binary $steam_options $grep`; - + logger "Getting latest version info for AppId $appId"; - + $ua->agent('OGP Windows Agent v/' . AGENT_VERSION); $ua->timeout(10); - + my $response = $ua->get("http://opengamepanel.org/supported_games/api.php?appid=$appId&action=getBuildId"); - + if ($response->is_success) { my $content = $response->decoded_content; - + if ($content =~ /^\d+\z/) { return $content; @@ -2173,7 +2328,7 @@ sub compress_files_without_decrypt logger "compress_files: Destination path ( $destination ) could not be found."; return -1; } - + chdir $destination; my @items = split /\Q\n/, $files; my @inventory; @@ -2191,7 +2346,7 @@ sub compress_files_without_decrypt elsif (-d $item) { $zip->addTree( $item, $item ); - } + } } } # Save the file @@ -2218,7 +2373,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2245,7 +2400,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2272,7 +2427,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2318,9 +2473,9 @@ sub discover_ips logger "Invalid parameter '$check' given for discover_ips function."; return ""; } - + my $iplist = ""; - + my @data = `ipconfig /all`; foreach my $temp (@data) @@ -2437,7 +2592,7 @@ sub restart_server_without_decrypt $control_password, $control_type, $home_path, $server_exe, $run_dir, $cmd, $cpu, $nice, $preStart, $envVars, $game_key, $console_log) = @_; - if (stop_server_without_decrypt($home_id, $server_ip, + if (stop_server_without_decrypt($home_id, $server_ip, $server_port, $control_protocol, $control_password, $control_type, $home_path) == 0) { @@ -2477,7 +2632,7 @@ sub sudo_exec_without_decrypt } sub secure_path -{ +{ return "1;"; } @@ -2491,7 +2646,7 @@ sub ftp_mgr no warnings 'uninitialized'; # recommended: only switch of specific categories return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($action, $login, $password, $home_path) = decrypt_params(@_); - + if(!defined($Cfg::Preferences{ogp_manages_ftp}) || (defined($Cfg::Preferences{ogp_manages_ftp}) && $Cfg::Preferences{ogp_manages_ftp} eq "1")){ if( defined($Cfg::Preferences{ftp_method}) && $Cfg::Preferences{ftp_method} eq "FZ") { @@ -2504,7 +2659,7 @@ sub ftp_mgr require XML::Simple; my $xml = new XML::Simple; - my $data = $xml->XMLin( $Cfg::FileZilla{fz_xml}, + my $data = $xml->XMLin( $Cfg::FileZilla{fz_xml}, ForceArray => ['User','Permission','IpFilter','Allowed','Disallowed','IP','Item'], ForceContent => 0, KeepRoot => 1, @@ -2826,16 +2981,16 @@ sub ftp_mgr return 1; } elsif( defined($Cfg::Preferences{ftp_method}) && $Cfg::Preferences{ftp_method} eq "PureFTPd") - { + { my $uid = `id -u`; chomp $uid; my $gid = `id -g`; chomp $gid; - + $login =~ s/('+)/'\"$1\"'/g; $password =~ s/('+)/'\"$1\"'/g; $home_path =~ s/('+)/'\"$1\"'/g; - + if($action eq "list") { return sudo_exec_without_decrypt("pure-pw list"); @@ -2859,18 +3014,18 @@ sub ftp_mgr elsif($action eq "usermod") { my $update_account = "pure-pw usermod '$login' -u $uid -g $gid"; - + my @account_settings = split /[\n]+/, $password; - + foreach my $setting (@account_settings) { my ($key, $value) = split /[\t]+/, $setting; - + if( $key eq 'Directory' ) { $value =~ s/('+)/'\"$1\"'/g; $update_account .= " -d '$value'"; } - + if( $key eq 'Full_name' ) { if( $value ne "" ) @@ -2883,7 +3038,7 @@ sub ftp_mgr $update_account .= ' -c ""'; } } - + if( $key eq 'Download_bandwidth' && $value ne "" ) { my $Download_bandwidth; @@ -2897,7 +3052,7 @@ sub ftp_mgr } $update_account .= " -t " . $Download_bandwidth; } - + if( $key eq 'Upload___bandwidth' && $value ne "" ) { my $Upload___bandwidth; @@ -2911,7 +3066,7 @@ sub ftp_mgr } $update_account .= " -T " . $Upload___bandwidth; } - + if( $key eq 'Max_files' ) { if( $value eq "0" ) @@ -2927,7 +3082,7 @@ sub ftp_mgr $update_account .= ' -n ""'; } } - + if( $key eq 'Max_size' ) { if( $value ne "" && $value ne "0" ) @@ -2939,24 +3094,24 @@ sub ftp_mgr $update_account .= ' -N ""'; } } - + if( $key eq 'Ratio' && $value ne "" ) { my($upload_ratio,$download_ratio) = split/:/,$value; - + if($upload_ratio eq "0") { $upload_ratio = "\"\""; } $update_account .= " -q " . $upload_ratio; - + if($download_ratio eq "0") { $download_ratio = "\"\""; } $update_account .= " -Q " . $download_ratio; } - + if( $key eq 'Allowed_client_IPs' ) { if( $value ne "" ) @@ -2968,7 +3123,7 @@ sub ftp_mgr $update_account .= ' -r ""'; } } - + if( $key eq 'Denied__client_IPs' ) { if( $value ne "" ) @@ -2980,7 +3135,7 @@ sub ftp_mgr $update_account .= ' -R ""'; } } - + if( $key eq 'Allowed_local__IPs' ) { if( $value ne "" ) @@ -2992,7 +3147,7 @@ sub ftp_mgr $update_account .= ' -i ""'; } } - + if( $key eq 'Denied__local__IPs' ) { if( $value ne "" ) @@ -3004,13 +3159,13 @@ sub ftp_mgr $update_account .= ' -I ""'; } } - - + + if( $key eq 'Max_sim_sessions' && $value ne "" ) { $update_account .= " -y " . $value; } - + if ( $key eq 'Time_restrictions' ) { if( $value eq "0000-0000") @@ -3083,18 +3238,18 @@ sub stop_fastdl_without_decrypt sub take_ownership{ # Looks like this is required to make sure that permissions are correct... my ($home_path, $action) = @_; - + my $icaclsStr = ""; my $icaclsAdminGroupFullPerms = ""; my $takeownCommand = ""; my $chmodCommand = ""; my $fullCommands = ""; - + if (defined $home_path && $home_path ne "" && -e "$home_path"){ my $windows_home_path = clean(`cygpath -wa $home_path`); - + logger "Running takeown commands on path of $home_path and $windows_home_path"; - + #cygwin path handling $takeownCommand = 'takeown /U ' . USER_RUNNING_SCRIPT . ' /f "' . $home_path . '" /r >/dev/null 2>&1'; $chmodCommand = 'chmod 775 -R "' . $home_path . '" >/dev/null 2>&1'; @@ -3105,14 +3260,14 @@ sub take_ownership{ system($takeownCommand); system($chmodCommand); } - + # Windows path handling if(defined $windows_home_path && $windows_home_path ne ""){ $takeownCommand = 'takeown /U ' . USER_RUNNING_SCRIPT . ' /f "' . $windows_home_path . '" /r >/dev/null 2>&1'; $chmodCommand = 'chmod 775 -R "' . $windows_home_path . '" >/dev/null 2>&1'; $icaclsStr = 'icacls "' . $windows_home_path . '" /grant ' . USER_RUNNING_SCRIPT . ':\\(OI\\)\\(CI\\)F /T >/dev/null 2>&1'; $icaclsAdminGroupFullPerms = 'icacls "' . $windows_home_path . '" /grant administrators:F /T >/dev/null 2>&1'; - + if(defined $action && $action eq "str"){ $fullCommands .= $takeownCommand . "\n"; $fullCommands .= $chmodCommand . "\n"; @@ -3126,13 +3281,13 @@ sub take_ownership{ system($icaclsStr); system($icaclsAdminGroupFullPerms); } - } + } } - + if($fullCommands ne ""){ return $fullCommands; } - + return 1; } @@ -3144,10 +3299,10 @@ sub clean { return $text; } -sub trim { - my $s = shift; - $s =~ s/^\s+|\s+$//g; - return $s +sub trim { + my $s = shift; + $s =~ s/^\s+|\s+$//g; + return $s }; sub restart_fastdl @@ -3352,7 +3507,7 @@ sub agent_restart { my $init_pid = `cat ogp_agent_run.pid`; chomp($init_pid); - + if(kill 0, $init_pid) { my $or_exist = ""; @@ -3360,17 +3515,17 @@ sub agent_restart my $agent_pid = ""; my $restart_scr_log = Path::Class::File->new(SCREEN_LOGS_DIR, 'screenlog.agent_restart'); my $agent_scr_log = Path::Class::File->new(SCREEN_LOGS_DIR, 'screenlog.ogp_agent'); - + if(-e $restart_scr_log) { unlink $restart_scr_log; } - + if(-e $agent_scr_log) { unlink $agent_scr_log; } - + if(-e "ogp_agent.pid") { $rm_pid_file .= " ogp_agent.pid"; @@ -3381,7 +3536,7 @@ sub agent_restart $or_exist .= " -o -e /proc/$agent_pid"; } } - + my $pureftpd_pid = ""; if(-e "/var/run/pure-ftpd.pid") { @@ -3393,7 +3548,7 @@ sub agent_restart $or_exist .= " -o -e /proc/$pureftpd_pid"; } } - + open (AGENT_RESTART_SCRIPT, '>', 'tmp_restart.sh'); my $restart = "echo -n \"Stopping OGP Agent...\"\n". "kill $init_pid $agent_pid $pureftpd_pid\n". @@ -3501,7 +3656,7 @@ sub scheduler_add_task print TASKS "$new_task\n"; logger "Created new task: $new_task"; close(TASKS); - scheduler_stop(); + scheduler_stop(); # Create new object with default dispatcher for scheduled tasks $cron = new Schedule::Cron( \&scheduler_dispatcher, { nofork => 1, @@ -3619,10 +3774,10 @@ sub scheduler_read_tasks scheduler_stop(); return -1; } - + my $i = 0; while () - { + { next if $_ =~ /^(#.*|[\s|\t]*?\n)/; my ($minute, $hour, $dayOfTheMonth, $month, $dayOfTheWeek, @args) = split(' ', $_); my $time = "$minute $hour $dayOfTheMonth $month $dayOfTheWeek"; @@ -3698,14 +3853,14 @@ sub get_file_part logger "ERROR - Can't open file $file for reading."; return -1; } - + binmode(FILE); - - if($offset != 0) + + if($offset != 0) { return -1 unless seek FILE, $offset, 0; } - + my $data = ""; my ($n, $buf); my $limit = $offset + 60 * 57 * 1000; #Max 3420Kb (1000 iterations) (top statistics ~ VIRT 116m, RES 47m) @@ -3714,7 +3869,7 @@ sub get_file_part $offset += $n; } close(FILE); - + if( $data ne "" ) { my $b64zlib = encode_base64(compress($data,9)); @@ -3743,7 +3898,7 @@ sub shell_action { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($action, $arguments) = decrypt_params(@_); - + if($action eq 'remove_file') { chomp($arguments); @@ -3951,7 +4106,7 @@ sub remote_query chdir($php_query_dir->subdir('lgsl')); my $cmd = $PHP_CGI . " -f lgsl_feed.php" . - " lgsl_type=" . $game_type . + " lgsl_type=" . $game_type . " ip=" . $ip . " c_port=" . $c_port . " q_port=" . $q_port . @@ -3971,7 +4126,7 @@ sub remote_query chdir($php_query_dir->subdir('gameq')); my $cmd = $PHP_CGI . " -f gameq_feed.php" . - " game_type=" . $game_type . + " game_type=" . $game_type . " ip=" . $ip . " c_port=" . $c_port . " q_port=" . $q_port . @@ -4016,29 +4171,29 @@ sub steam_workshop_without_decrypt my ($home_id, $mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list, $anonymous_login, $user, $pass, $download_method, $url_list, $filename_list) = @_; - + # Creates mods path if it doesn't exist if ( check_b4_chdir($mods_full_path) != 0) { return -1; } - + my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); my @workshop_mods = split /,/, $mods_list; my @installcmds; - + if($download_method eq 'steamcmd') { my $steam_binary = STEAMCMD_CLIENT_BIN; - my $installSteamFile = $screen_id . "_workshop.txt"; + my $installSteamFile = $screen_id . "_workshop.txt"; my $installtxt = Path::Class::File->new(STEAMCMD_CLIENT_DIR, $installSteamFile); my $windows_mods_path = clean(`cygpath -wa $mods_full_path`); - + open FILE, '>', $installtxt; print FILE "\@ShutdownOnFailedCommand 1\n"; print FILE "\@NoPromptForPassword 1\n"; @@ -4061,7 +4216,7 @@ sub steam_workshop_without_decrypt $windows_installtxt =~ s/\\/\\\\/g; @installcmds = ("$steam_binary +runscript $windows_installtxt +exit"); } - + if($download_method eq 'steamapi') { my @urls = split /,/, $url_list; @@ -4070,7 +4225,7 @@ sub steam_workshop_without_decrypt foreach my $workshop_mod_id (@workshop_mods) { my $steamcmd_download_path = '/steamapps/workshop/content/'.$workshop_id.'/'.$workshop_mod_id.'/'; - + my $workshop_mod_path = $mods_full_path.$steamcmd_download_path; if(!-d $workshop_mod_path && !mkpath $workshop_mod_path) { @@ -4085,33 +4240,33 @@ sub steam_workshop_without_decrypt $index++; } } - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log($home_id, $log_file); - + my $precmd = ""; my $postcmd = ""; - + $postcmd .= generate_post_install_scripts($mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list); - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path) != 0) { return -1; } - + my $installfile = create_bash_scripts($mods_full_path, $bash_scripts_path, $precmd, $postcmd, @installcmds); - + my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); - + logger "Installing Steam Workshop content on server Home ID " . $home_id; system($screen_cmd); - + return 1; } @@ -4119,10 +4274,10 @@ sub generate_post_install_scripts { my ($mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list) = @_; - + my $post_install_scripts = ""; my $mods_info_path = Path::Class::Dir->new(AGENT_RUN_DIR, 'WorkshopModsInfo'); $post_install_scripts .= "mods_full_path=\"$mods_full_path\"\n". @@ -4136,7 +4291,7 @@ sub generate_post_install_scripts "mods_info_path=\"$mods_info_path/\"\n"; my @workshop_mods = split /,/, $mods_list; my @mod_names = split /,/, $mod_names_list; - + my $index = 0; foreach my $workshop_mod_id (@workshop_mods) { @@ -4144,14 +4299,14 @@ sub generate_post_install_scripts my $workshop_mod_path = $mods_full_path.$steamcmd_download_path; my $this_mod_string = $mod_string; $this_mod_string =~ s/\%workshop_mod_id\%/$workshop_mod_id/g; - + $post_install_scripts .= "mod_string[$index]=\"$this_mod_string\"\n". "mod_name[$index]=\"".$mod_names[$index]."\"\n". "workshop_mod_id[$index]=\"$workshop_mod_id\"\n". "workshop_mod_path[$index]=\"$workshop_mod_path\"\n"; $index++; } - + $post_install_scripts .= 'if [ ! -e $config_file_path ];then'."\n". ' if [ ! -d "$(dirname $config_file_path)" ];then mkdir -p "$(dirname $config_file_path)";fi'."\n". ' echo -e "${place_after}\n${variable}" > $config_file_path'."\n". @@ -4160,7 +4315,7 @@ sub generate_post_install_scripts 'for mod_id in "${workshop_mod_id[@]}"'."\n". 'do'."\n". ' first_file="$(ls "${workshop_mod_path[$i]}"| sort -n | head -1)"'."\n"; - + my @post_install_lines = split /[\r\n]+/, $post_install; foreach my $line (@post_install_lines) { if($line ne ""){ @@ -4170,7 +4325,7 @@ sub generate_post_install_scripts $post_install_scripts .= "\t".$line."\n"; } } - + $post_install_scripts .= ' file_content=$(cat $config_file_path)'."\n". ' if [[ $file_content =~ $regex ]]; then'."\n". ' full_match="${BASH_REMATCH[0]}"'."\n". @@ -4180,7 +4335,7 @@ sub generate_post_install_scripts ' found=0'."\n". ' fi'."\n". ' first_file_string="\%first_file%"'."\n". - + ' if [ -z "${mod_string[$i]##*$first_file_string*}" ];then'."\n". ' mod_string[$i]="${mod_string[$i]/$first_file_string/$first_file}"'."\n". ' fi'."\n". @@ -4217,9 +4372,9 @@ sub generate_post_install_scripts sub get_workshop_mods_info() { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); - + my $mods_info_dir_path = Path::Class::Dir->new(AGENT_RUN_DIR, 'WorkshopModsInfo'); - + if(-d $mods_info_dir_path) { opendir(MODS_INFO_DIR, $mods_info_dir_path) or return -1; @@ -4245,14 +4400,14 @@ sub get_workshop_mods_info() closedir(MODS_INFO_DIR); return "1;".encode_list(@mods_info); } - + return -1; } sub get_setting_using_api { my ($setting_name) = @_; - + if(defined WEB_API_URL && WEB_API_URL ne "" && defined WEB_ADMIN_API_KEY && WEB_ADMIN_API_KEY ne ""){ my $url = WEB_API_URL . "?setting/get&setting_name=" . $setting_name . "&token=" . WEB_ADMIN_API_KEY; my $ua = LWP::UserAgent->new; @@ -4262,7 +4417,7 @@ sub get_setting_using_api my $response = $ua->get($url); return $response->decoded_content(); } - + return -1; } @@ -4286,7 +4441,7 @@ sub get_minecraft_rcon_port{ } close(FH); - + return $port; } diff --git a/Agent_Linux/ogp_agent.pl b/Agent_Linux/ogp_agent.pl index b28a7428..e26a9011 100644 --- a/Agent_Linux/ogp_agent.pl +++ b/Agent_Linux/ogp_agent.pl @@ -182,7 +182,7 @@ if (! -e SCREENRC_FILE) open INPUTFILE, "<", SCREENRC_FILE or die $!; open OUTPUTFILE, ">", SCREENRC_TMP_FILE or die $!; my $dest = SCREEN_LOGS_DIR . "/screenlog.%t"; -while () +while () { $_ =~ s/logfile.*/logfile $dest/g; print OUTPUTFILE $_; @@ -312,7 +312,7 @@ logger "Open Game Panel - Agent started - " . " - PID $$", 1; # Stop previous scheduler process if exists -scheduler_stop(); +scheduler_stop(); # Create new object with default dispatcher for scheduled tasks my $cron = new Schedule::Cron( \&scheduler_dispatcher, { nofork => 1, @@ -364,8 +364,9 @@ if(-e Path::Class::File->new(FD_DIR, 'Settings.pm')) my $d = Frontier::Daemon::OGP::Forking->new( methods => { - is_screen_running => \&is_screen_running, - universal_start => \&universal_start, + is_screen_running => \&is_screen_running, + server_status => \&server_status, + universal_start => \&universal_start, renice_process => \&renice_process, cpu_count => \&cpu_count, rfile_exists => \&rfile_exists, @@ -431,9 +432,9 @@ my $d = Frontier::Daemon::OGP::Forking->new( sub backup_home_log { my ($home_id, $log_file, $console_log_file) = @_; - + my $home_backup_dir = SCREEN_LOGS_DIR . "/home_id_" . $home_id; - + if( ! -e $home_backup_dir ) { if( ! mkdir $home_backup_dir ) @@ -442,13 +443,13 @@ sub backup_home_log return 1; } } - + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - + my $backup_file_name = $mday . $mon . $year . '_' . $hour . 'h' . $min . 'm' . $sec . "s.log"; - + my $output_path = $home_backup_dir . "/" . $backup_file_name; - + # Used for deleting log files older than DELETE_LOGS_AFTER my @file_list; my @find_dirs; # directories to search @@ -462,37 +463,37 @@ sub backup_home_log my $seconds_per_day = 60*60*24; # seconds in a day my $AGE = $days*$seconds_per_day; # age in seconds push (@find_dirs, $home_backup_dir); - - # Create local copy of log file backup in the log_backups folder and current user home directory if SCREEN_LOG_LOCAL = 1 + + # Create local copy of log file backup in the log_backups folder and current user home directory if SCREEN_LOG_LOCAL = 1 if(SCREEN_LOG_LOCAL == 1) { # Create local backups folder my $local_log_folder = Path::Class::Dir->new("logs_backup"); - + if(!-e $local_log_folder){ mkdir($local_log_folder); } - + # Add full path to @find_dirs so that log files older than DELETE_LOGS_AFTER are deleted my $fullpath_to_local_logs = Path::Class::Dir->new(getcwd(), "logs_backup"); push (@find_dirs, $fullpath_to_local_logs); - + my $log_local = $local_log_folder . "/" . $backup_file_name; - + # Delete the local log file if it already exists if(-e $log_local){ unlink $log_local; } - + # If the log file contains UPDATE in the filename, do not allow users to see it since it will contain steam credentials # Will return -1 for not existing my $isUpdate = index($log_file,SCREEN_TYPE_UPDATE); - + if($isUpdate == -1){ copy($log_file,$log_local); } } - + # Delete all files in @find_dirs older than DELETE_LOGS_AFTER days find ( sub { my $file = $File::Find::name; @@ -500,14 +501,14 @@ sub backup_home_log push (@file_list, $file); } }, @find_dirs); - + # Include the custom console path - and also do a size check on it if(defined $console_log_file && $console_log_file ne ""){ my $path_to_console_file = $console_log_file; if( -f $path_to_console_file){ push (@file_list, $path_to_console_file); - - # Backup and delete this specific file as well if it's over 20MB + + # Backup and delete this specific file as well if it's over 20MB my @stats = stat($path_to_console_file); if($stats[7] >= 20971520){ if(SCREEN_LOG_LOCAL == 1){ @@ -521,7 +522,7 @@ sub backup_home_log } } } - + for my $file (@file_list) { if( -f $file ){ my @stats = stat($file); @@ -530,9 +531,9 @@ sub backup_home_log } } } - + move($log_file,$output_path); - + return 0; } @@ -541,9 +542,9 @@ sub get_home_pids my ($home_id) = @_; my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); my ($pid, @pids); - + my $as_user = find_user_by_screen_id($screen_id); - + my $ret = sudo_exec_without_decrypt('screen -ls | grep -E -o "[0-9]+\.'.$screen_id.'"', $as_user); my ($retval, $enc_out) = split(/;/, $ret, 2); if($retval != 1) @@ -551,11 +552,11 @@ sub get_home_pids logger "Unable to get pids, probably a bad sudo password or not in sudoers list."; return (); } - + $enc_out =~ s/\\n//g; - + my $out = decode_base64($enc_out); - + ($pid) = split(/\./, $out, 2); if(defined $pid) { @@ -590,40 +591,40 @@ sub create_screen_cmd_loop { my ($screen_id, $exec_cmd, $envVars, $skipLoop) = @_; my $server_start_bashfile = $screen_id . "_startup_scr.sh"; - + $exec_cmd = replace_OGP_Env_Vars($screen_id, "", "", $exec_cmd); - + # Allow file to be overwritten if(-e $server_start_bashfile){ secure_path_without_decrypt('chattr-i', $server_start_bashfile); } - + # Create bash file that screen will run which spawns the server # If it crashes without user intervention, it will restart open (SERV_START_SCRIPT, '>', $server_start_bashfile); - + my $respawn_server_command = "#!/bin/bash" . "\n"; - + if(!$skipLoop){ $respawn_server_command .= "function startServer(){" . "\n"; } - + if(defined $envVars && $envVars ne ""){ $respawn_server_command .= $envVars; } - + if(!$skipLoop){ $respawn_server_command .= "NUMSECONDS=`expr \$(date +%s)`" . "\n" - . "until " . $exec_cmd . "; do" . "\n" + . "until " . $exec_cmd . "; do" . "\n" . "let DIFF=(`date +%s` - \"\$NUMSECONDS\")" . "\n" - . "if [ \"\$DIFF\" -gt 15 ]; then" . "\n" + . "if [ \"\$DIFF\" -gt 15 ]; then" . "\n" . "NUMSECONDS=`expr \$(date +%s)`" . "\n" - . "echo \"Server '" . $exec_cmd . "' crashed with exit code \$?. Respawning...\" >&2 " . "\n" - . "fi" . "\n" - . "sleep 3" . "\n" - . "done" . "\n" + . "echo \"Server '" . $exec_cmd . "' crashed with exit code \$?. Respawning...\" >&2 " . "\n" + . "fi" . "\n" + . "sleep 3" . "\n" + . "done" . "\n" . "let DIFF=(`date +%s` - \"\$NUMSECONDS\")" . "\n" - + . "if [ ! -e \"SERVER_STOPPED\" ] && [ \"\$DIFF\" -gt 15 ]; then" . "\n" . "startServer" . "\n" . "fi" . "\n" @@ -632,19 +633,19 @@ sub create_screen_cmd_loop }else{ $respawn_server_command .= $exec_cmd . "\n"; } - + print SERV_START_SCRIPT $respawn_server_command; close (SERV_START_SCRIPT); - + # Make it not readable my $readOnlyOwnerCmd = "chmod -Rf og-r '$server_start_bashfile'"; sudo_exec_without_decrypt($readOnlyOwnerCmd); - + # Secure file secure_path_without_decrypt('chattr+i', $server_start_bashfile); - + my $screen_exec_script = "bash " . $server_start_bashfile; - + return sprintf('export WINEDEBUG="fixme-all" && export DISPLAY=:1 && screen -d -m -t "%1$s" -c ' . SCREENRC_FILE . ' -S %1$s %2$s', $screen_id, $screen_exec_script); @@ -659,21 +660,21 @@ sub handle_lock_command_line{ return secure_path_without_decrypt("chattr+i", $command); } } - + return 0; } sub replace_OGP_Env_Vars{ # This function replaces constants from environment variables set in the XML my ($screen_id, $homeid, $homepath, $exec_cmd, $game_key) = @_; - + # Handle steam specific replacements if(defined $screen_id && $screen_id ne ""){ my $screen_id_for_txt_update = substr ($screen_id, rindex($screen_id, '_') + 1); my $steamInsFile = $screen_id_for_txt_update . "_install.txt"; my $steamCMDPath = STEAMCMD_CLIENT_DIR; my $fullPath = Path::Class::File->new($steamCMDPath, $steamInsFile); - + # If the install file exists, the game can be auto updated, else it will be ignored by the game for improper syntax # To generate the install file, the "Install/Update via Steam" button must be clicked on at least once! if(-e $fullPath){ @@ -682,15 +683,15 @@ sub replace_OGP_Env_Vars{ } } - + # Handle home directory replacement if(defined $homepath && $homepath ne ""){ $exec_cmd =~ s/{OGP_HOME_DIR}/$homepath/g; } - + # Handle global game shared directory replacement if(defined $game_key && $game_key ne ""){ - my $readable_game_key = lc(substr($game_key, 0, rindex($game_key,"_"))); + my $readable_game_key = lc(substr($game_key, 0, rindex($game_key,"_"))); my $shared_path = Path::Class::Dir->new(SHARED_GAME_TMP_DIR, $readable_game_key); # Create the folder if it doesn't exist if (!-d $shared_path && !mkdir $shared_path) @@ -699,7 +700,7 @@ sub replace_OGP_Env_Vars{ } $exec_cmd =~ s/{OGP_GAME_SHARED_DIR}/$shared_path/g; } - + return $exec_cmd; } @@ -764,11 +765,11 @@ sub check_steam_cmd_client "http://media.steampowered.com/client/" . $steam_client_file; logger "Downloading the Steam client from $steam_client_url to '" . $steam_client_path . "'."; - + my $ua = LWP::UserAgent->new; $ua->agent('Mozilla/5.0'); my $response = $ua->get($steam_client_url, ':content_file' => "$steam_client_path"); - + unless ($response->is_success) { logger "Failed to download steam installer from " @@ -810,26 +811,26 @@ sub is_screen_running_without_decrypt my ($screen_type, $home_id) = @_; my $screen_id = create_screen_id($screen_type, $home_id); - + my $as_user = find_user_by_screen_id($screen_id); - + my $ret = sudo_exec_without_decrypt('screen -list | grep '.$screen_id, $as_user); - + my ($retval, $enc_out) = split(/;/, $ret, 2); - + if($retval != 1) { return 0; } - + my $is_running = " "; - + if( defined($enc_out) ) { $enc_out =~ s/\\n//g; $is_running = decode_base64($enc_out); } - + if ($is_running =~ /^\s*$/) { return 0; @@ -840,6 +841,149 @@ sub is_screen_running_without_decrypt } } +sub server_status +{ + return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); + return server_status_without_decrypt(decrypt_params(@_)); +} + +sub get_status_hint_path +{ + my ($home_id) = @_; + $home_id =~ s/[^0-9]//g; + return Path::Class::File->new(GAME_STARTUP_DIR, "status-$home_id"); +} + +sub write_status_hint +{ + my ($home_id, $state) = @_; + my $hint_file = get_status_hint_path($home_id); + if(open(STATUSHINT, '>', $hint_file)) + { + print STATUSHINT time() . "," . $state; + close(STATUSHINT); + } +} + +sub read_status_hint +{ + my ($home_id) = @_; + my $hint_file = get_status_hint_path($home_id); + return (0, "") unless(-e $hint_file); + open(STATUSHINT, '<', $hint_file) or return (0, ""); + my $line = ; + close(STATUSHINT); + chomp($line); + my ($timestamp, $state) = split(/,/, $line, 2); + $timestamp = 0 unless(defined($timestamp) && $timestamp =~ /^\d+$/); + $state = "" unless(defined($state)); + return ($timestamp, $state); +} + +sub get_screen_pid_without_decrypt +{ + my ($home_id) = @_; + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); + my $as_user = find_user_by_screen_id($screen_id); + my $ret = sudo_exec_without_decrypt('screen -list | grep '.$screen_id, $as_user); + my ($retval, $enc_out) = split(/;/, $ret, 2); + return "" if($retval != 1 || !defined($enc_out)); + $enc_out =~ s/\\n//g; + my $screen_line = decode_base64($enc_out); + return $1 if($screen_line =~ /([0-9]+)\.$screen_id/); + return ""; +} + +sub is_port_listening_without_decrypt +{ + my ($server_ip, $port) = @_; + return 0 unless(defined($port) && $port =~ /^[0-9]+$/ && $port > 0 && $port <= 65535); + my @commands = ( + "ss -lntu 2>/dev/null", + "netstat -lntu 2>/dev/null", + "lsof -nP -iTCP:$port -sTCP:LISTEN 2>/dev/null", + "lsof -nP -iUDP:$port 2>/dev/null" + ); + foreach my $command (@commands) + { + my $out = `$command`; + next unless(defined($out) && $out ne ""); + return 1 if($out =~ /[:.]$port(\s|$)/); + } + return 0; +} + +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]+$/); + $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 ($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 $ready = 0; + + if($session_running && $game_port_listening) + { + $status = "ONLINE"; + $ready = 1; + } + elsif($session_running) + { + if($effective_hint eq "STOPPING") + { + $status = "STOPPING"; + } + 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."; + } + else + { + $status = "STARTING"; + } + } + elsif($game_port_listening) + { + $status = "UNRESPONSIVE"; + $last_error = "Game port is listening but the managed screen session is not running."; + } + else + { + $status = "OFFLINE"; + } + + return { + status => $status, + ready => $ready, + 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, + pid => $pid, + session_name => $screen_id, + ip => $server_ip, + port => $server_port, + query_port => $query_port, + rcon_port => $rcon_port, + last_error => $last_error, + query_info => "" + }; +} + # Delete Server Stopped Status File: sub deleteStoppedStatFile { @@ -866,50 +1010,50 @@ sub universal_start_without_decrypt $home_id, $home_path, $server_exe, $run_dir, $startup_cmd, $server_port, $server_ip, $cpu, $nice, $preStart, $envVars, $game_key, $console_log ) = @_; - + if (is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1) { logger "This server is already running (ID: $home_id)."; return -14; } - + if (!-e $home_path) { logger "Can't find server's install path [ $home_path ]."; return -10; } - + my $owner = SERVER_RUNNER_USER; my $group = SERVER_RUNNER_USER; my $ogpAgentGroup = `whoami`; - + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); chomp $ogpAgentGroup; - + if(defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ $owner = "gamehome" . $home_id; $group = `whoami`; chomp $group; - + # Create new user if doesn't exist my $userExists = `id -u $owner`; if(not is_integer($userExists)){ logger "User $owner currently doesn't exist... creating user..."; - - sudo_exec_without_decrypt("useradd -m $owner"); - sudo_exec_without_decrypt("usermod -s /bin/bash $owner"); - sudo_exec_without_decrypt("usermod -a -G \"$owner\" \"$group\""); + + sudo_exec_without_decrypt("useradd -m $owner"); + sudo_exec_without_decrypt("usermod -s /bin/bash $owner"); + sudo_exec_without_decrypt("usermod -a -G \"$owner\" \"$group\""); } } - + # Fix perms on ogp_agent user's homedir so that other users can access their owned files within this dir my $fixOGPHomeDirCommand = 'chmod -R ug+rwx $( getent passwd "' . $ogpAgentGroup . '" | cut -d: -f6 )'; sudo_exec_without_decrypt($fixOGPHomeDirCommand); $fixOGPHomeDirCommand = 'find "$( getent passwd "' . $ogpAgentGroup . '" | cut -d: -f6 )" -type d -print0 | xargs -0 chmod o+x'; sudo_exec_without_decrypt($fixOGPHomeDirCommand); - + # Some game require that we are in the directory where the binary is. my $game_binary_dir = Path::Class::Dir->new($home_path, $run_dir); if ( -e $game_binary_dir && !chdir $game_binary_dir) @@ -917,13 +1061,13 @@ sub universal_start_without_decrypt logger "Could not change to server binary directory $game_binary_dir."; return -12; } - + # Temporarily unlock the server executable secure_path_without_decrypt('chattr-i', $server_exe); - + # Set ownership on the game home set_path_ownership($owner, $group, $home_path, 1); - + if (!-x $server_exe) { if (!chmod 0775, $server_exe) @@ -932,14 +1076,14 @@ sub universal_start_without_decrypt return -13; } } - + if(defined $preStart && $preStart ne ""){ # Get it in the format that the startup file can use $preStart = multiline_to_startup_comma_format($preStart); }else{ $preStart = ""; } - + if(defined $envVars && $envVars ne ""){ # Replace variables in the envvars if they exist my @prestartenvvars = split /[\r\n]+/, $envVars; @@ -951,20 +1095,20 @@ sub universal_start_without_decrypt $envVarStr .= "$line\n"; } } - + if(defined $envVarStr && $envVarStr ne ""){ $envVars = $envVarStr; - } - + } + # Get it in the format that the startup file can use $envVars = multiline_to_startup_comma_format($envVars); }else{ $envVars = ""; } - + # Secure file secure_path_without_decrypt('chattr+i', $server_exe); - + # Create startup file for the server. my $startup_file = Path::Class::File->new(GAME_STARTUP_DIR, "$server_ip-$server_port"); @@ -979,39 +1123,39 @@ sub universal_start_without_decrypt { logger "Cannot create file in " . $startup_file . " : $!"; } - + if(defined $preStart && $preStart ne ""){ # Get it in the format that the startup file can use $preStart = startup_comma_format_to_multiline($preStart); }else{ $preStart = ""; } - + if(defined $envVars && $envVars ne ""){ # Get it in the format that the startup file can use - $envVars = startup_comma_format_to_multiline($envVars); + $envVars = startup_comma_format_to_multiline($envVars); }else{ $envVars = ""; } - + # Create the startup string. my $file_extension = substr $server_exe, -4; my $cli_bin; my $command; my $run_before_start; - + # Replace any OGP variables found in the command line $startup_cmd = replace_OGP_Env_Vars($screen_id, $home_id, $home_path, $startup_cmd, $game_key); - + if($file_extension eq ".exe" or $file_extension eq ".bat") { $command = "wine $server_exe $startup_cmd"; - + if ($cpu ne 'NA') { $command = "taskset -c $cpu wine $server_exe $startup_cmd"; } - + if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ deleteStoppedStatFile($home_path); $cli_bin = create_screen_cmd_loop($screen_id, $command, $envVars); @@ -1022,12 +1166,12 @@ sub universal_start_without_decrypt elsif($file_extension eq ".jar") { $command = "$startup_cmd"; - + if ($cpu ne 'NA') { $command = "taskset -c $cpu $startup_cmd"; } - + if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ deleteStoppedStatFile($home_path); $cli_bin = create_screen_cmd_loop($screen_id, $command, $envVars); @@ -1038,12 +1182,12 @@ sub universal_start_without_decrypt else { $command = "./$server_exe $startup_cmd"; - + if ($cpu ne 'NA') { $command = "taskset -c $cpu ./$server_exe $startup_cmd"; } - + if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ deleteStoppedStatFile($home_path); $cli_bin = create_screen_cmd_loop($screen_id, $command, $envVars); @@ -1051,13 +1195,13 @@ sub universal_start_without_decrypt $cli_bin = create_screen_cmd_loop($screen_id, $command, $envVars, 1); } } - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log( $home_id, $log_file, $home_path . "/" . $console_log ); - + logger "Startup command [ $cli_bin ] will be executed in dir $game_binary_dir."; - + # Fix permissions one last time (for backup_home_log created folder / files / etc) my $server_start_bashfile = $screen_id . "_startup_scr.sh"; secure_path_without_decrypt('chattr-i', $server_start_bashfile); @@ -1065,18 +1209,19 @@ sub universal_start_without_decrypt my $readOnlyOwnerCmd = "chmod -Rf og-r '$server_start_bashfile'"; sudo_exec_without_decrypt($readOnlyOwnerCmd); secure_path_without_decrypt('chattr+i', $server_start_bashfile); - + # Run before start script $run_before_start = run_before_start_commands($home_id, $home_path, $preStart, $owner); - + sudo_exec_without_decrypt($cli_bin, $owner); - + write_status_hint($home_id, "STARTING"); + sleep(1); - + renice_process_without_decrypt($home_id, $nice); - + chdir AGENT_RUN_DIR; - + return 1; } @@ -1091,9 +1236,9 @@ sub renice_process sub renice_process_without_decrypt { - my ($home_id, $nice) = @_; + my ($home_id, $nice) = @_; if ($nice != 0) - { + { my @pids = get_home_pids($home_id); logger "Renicing pids [ @pids ] from home_id $home_id with nice value $nice."; @@ -1239,9 +1384,9 @@ sub get_log { $log_file = Path::Class::File->new($home_path, $log_file); } - + sudo_exec_without_decrypt("chmod 777 \"$log_file\""); - + # Create local copy of current log file if SCREEN_LOG_LOCAL = 1 if(SCREEN_LOG_LOCAL == 1) { @@ -1250,13 +1395,13 @@ sub get_log { unlink $log_local; } - + # Copy log file only if it's not an UPDATE type as it may contain steam credentials if($screen_type eq SCREEN_TYPE_HOME){ copy($log_file, $log_local); } } - + # Regenerate the log file if it doesn't exist unless ( -e $log_file ) { @@ -1272,12 +1417,12 @@ sub get_log return -8; } } - + # Return a few lines of output to the web browser my(@modedlines) = `tail -n $nb_of_lines $log_file`; - + my $linecount = 0; - + foreach my $line (@modedlines) { #Remove unwanted characters (https://superuser.com/questions/99128/removing-the-escape-characters-from-gnu-screens-screenlog-n) $line =~ s/\x1b[[()=][;?0-9]*[0-9A-Za-z]?//g; @@ -1296,8 +1441,8 @@ sub get_log $line =~ s/�//g; $modedlines[$linecount]=$line; $linecount++; - } - + } + my $encoded_content = encode_list(@modedlines); chdir AGENT_RUN_DIR; if(is_screen_running_without_decrypt($screen_type, $home_id) == 1) @@ -1325,21 +1470,22 @@ sub stop_server_without_decrypt { my ($home_id, $server_ip, $server_port, $control_protocol, $control_password, $control_type, $home_path) = @_; - + my $usedProtocolToStop = 0; - + write_status_hint($home_id, "STOPPING"); + my $startup_file = Path::Class::File->new(GAME_STARTUP_DIR, "$server_ip-$server_port"); - + if (-e $startup_file) { logger "Removing startup flag " . $startup_file . ""; unlink($startup_file) or logger "Cannot remove the startup flag file $startup_file $!"; } - + # Create file indicator that the game server has been stopped if defined if(defined($Cfg::Preferences{ogp_autorestart_server}) && $Cfg::Preferences{ogp_autorestart_server} eq "1"){ - + # Get current directory and chdir into the game's home dir my $curDir = getcwd(); chdir $home_path; @@ -1347,18 +1493,18 @@ sub stop_server_without_decrypt # Create stopped indicator file used by autorestart of OGP if server crashes open(STOPFILE, '>', "SERVER_STOPPED"); close(STOPFILE); - + # Return to original directory chdir $curDir; } - + # Some validation checks for the variables. if ($server_ip =~ /^\s*$/ || $server_port < 0 || $server_port > 65535) { logger("Invalid IP:Port given $server_ip:$server_port."); return 1; } - + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); my $as_user = find_user_by_screen_id($screen_id); @@ -1405,7 +1551,7 @@ sub stop_server_without_decrypt my $rconCommand = "#shutdown"; my $armabe_result = $armabe->run($rconCommand); if ($armabe_result) { - logger "ArmaBE Shutdown command sent successfully"; + logger "ArmaBE Shutdown command sent successfully"; $usedProtocolToStop = 1; } } @@ -1413,15 +1559,15 @@ sub stop_server_without_decrypt { use Minecraft::RCON; my $strip_color = 1; - + my $rconPort = get_minecraft_rcon_port($home_path); - + logger "Minecraft rcon port detected as $rconPort with path of $home_path"; - + if ($rconPort != -1){ my $minecraft; my $response; - + eval { $minecraft = Minecraft::RCON->new( { @@ -1432,19 +1578,19 @@ sub stop_server_without_decrypt } ); }; - + if (defined $minecraft) { eval { $minecraft->connect }; logger "Minecraft rcon module connection failed: $@" if $@; - - + + my $rconCommand = "/stop"; eval { $response = $minecraft->command($rconCommand) }; logger $@ ? "Minecraft rcon error: $@" : "Minecraft rcon module response: $response"; - + eval { $minecraft->disconnect; }; - + if (defined $response) { logger "Minecraft Shutdown command sent successfully"; $usedProtocolToStop = 1; @@ -1452,45 +1598,45 @@ sub stop_server_without_decrypt } } } - + my @server_pids; - + # Gives the server time to shutdown with rcon in case it takes a while for the server to shutdown (arma for example) before we forcefully kill it if ($usedProtocolToStop == 1 && is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1){ @server_pids = get_home_pids($home_id); my $timeWaited = 0; my $pidSize = @server_pids; my $maxWaitTime = 5; - + # Maximum time to wait can now be configured as a preference if(defined($Cfg::Preferences{protocol_shutdown_waittime}) && $Cfg::Preferences{protocol_shutdown_waittime} =~ /^\d+?$/){ $maxWaitTime = $Cfg::Preferences{protocol_shutdown_waittime}; } - + while ($pidSize > 0 && $timeWaited < $maxWaitTime && is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1) { select(undef, undef, undef, 0.25); # Sleeps for 250ms - + # Add to time waited $timeWaited += 0.25; - + # Recheck server home PIDs @server_pids = get_home_pids($home_id); $pidSize = @server_pids; } } - - if (is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 0) - { - logger "Stopped server $server_ip:$server_port with rcon quit."; - return 0; - } + + if (is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 0) + { + logger "Stopped server $server_ip:$server_port with rcon quit."; + return verify_server_stopped_without_decrypt($home_id, $server_ip, $server_port); + } else { logger "Failed to send rcon quit. Stopping server with kill command."; } - + @server_pids = get_home_pids($home_id); - + my $cnt; my $out; foreach my $pid (@server_pids) @@ -1516,14 +1662,14 @@ sub stop_server_without_decrypt logger "Stopped process with pid $pid successfully using kill 15."; } } - sudo_exec_without_decrypt('screen -wipe > /dev/null 2>&1', $as_user); - return 0; - } + sudo_exec_without_decrypt('screen -wipe > /dev/null 2>&1', $as_user); + return verify_server_stopped_without_decrypt($home_id, $server_ip, $server_port); + } else { logger "Remote control protocol not available or PASSWORD NOT SET. Using kill signal instead."; my @server_pids = get_home_pids($home_id); - + my $cnt; my $out; foreach my $pid (@server_pids) @@ -1549,12 +1695,37 @@ sub stop_server_without_decrypt logger "Stopped process with pid $pid successfully using kill 15."; } } - sudo_exec_without_decrypt('screen -wipe > /dev/null 2>&1', $as_user); - return 0; + sudo_exec_without_decrypt('screen -wipe > /dev/null 2>&1', $as_user); + return verify_server_stopped_without_decrypt($home_id, $server_ip, $server_port); + } } + +sub verify_server_stopped_without_decrypt +{ + my ($home_id, $server_ip, $server_port) = @_; + for(my $i = 0; $i < 30; $i++) + { + my $session_running = is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1 ? 1 : 0; + my $port_listening = is_port_listening_without_decrypt($server_ip, $server_port); + return 0 if(!$session_running && !$port_listening); + sleep 2; + } + my $screen_id = create_screen_id(SCREEN_TYPE_HOME, $home_id); + my $as_user = find_user_by_screen_id($screen_id); + sudo_exec_without_decrypt('screen -S '.$screen_id.' -X quit', $as_user); + sleep 2; + sudo_exec_without_decrypt('screen -wipe > /dev/null 2>&1', $as_user); + my $session_running = is_screen_running_without_decrypt(SCREEN_TYPE_HOME, $home_id) == 1 ? 1 : 0; + my $port_listening = is_port_listening_without_decrypt($server_ip, $server_port); + if($session_running || $port_listening) + { + logger "Server $server_ip:$server_port is still running or listening after stop escalation."; + return 1; + } + return 0; } -##### Send RCON command +##### Send RCON command ### Return 0 when error occurred on decryption. ### Return 1 on success sub send_rcon_command @@ -1570,7 +1741,7 @@ sub send_rcon_command my $as_user = find_user_by_screen_id($screen_id); my $ScreenCommand = 'screen -S '.$screen_id.' -p 0 -X stuff "'.$rconCommand.'$(printf \\\\r)"'; logger "Sending legacy console command to ".$screen_id.": \n$rconCommand \n ."; - my $ret = sudo_exec_without_decrypt($ScreenCommand, $as_user); + my $ret = sudo_exec_without_decrypt($ScreenCommand, $as_user); my ($retval, $enc_out) = split(/;/, $ret, 2); if($retval == 1) { @@ -1580,14 +1751,14 @@ sub send_rcon_command } return 0; } - + # Some validation checks for the variables. if ($server_ip =~ /^\s*$/ || $server_port < 0 || $server_port > 65535) { logger("Invalid IP:Port given $server_ip:$server_port."); return 0; } - + if ($control_password !~ /^\s*$/) { if ($control_protocol eq "rcon") @@ -1601,7 +1772,7 @@ sub send_rcon_command ); logger "Sending RCON command to $server_ip:$server_port: \n$rconCommand \n ."; - + my(@modedlines) = $rcon->execute($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1615,9 +1786,9 @@ sub send_rcon_command password => $control_password, timeout => 2 ); - + logger "Sending RCON command to $server_ip:$server_port: \n $rconCommand \n ."; - + my(@modedlines) = $rcon2->run($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1631,9 +1802,9 @@ sub send_rcon_command password => $control_password, timeout => 2 ); - + logger "Sending RCON command via ArmaBE module to $server_ip:$server_port: \n $rconCommand \n ."; - + my(@modedlines) = $armabe->run($rconCommand); my $encoded_content = encode_list(@modedlines); return "1;" . $encoded_content; @@ -1679,49 +1850,49 @@ sub dirlistfm { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my $datadir = &decrypt_param(@_); - + logger "Asked for dirlist of $datadir directory."; - + if (!-d $datadir) { logger "ERROR - Directory [ $datadir ] not found!"; return -1; } - + if (!opendir(DIR, $datadir)) { logger "ERROR - Can't open $datadir: $!"; return -2; } - + my $dir = $datadir; $dir =~ s/('+)/'"$1"'/g; my $lsattr = `lsattr '$dir' 2>/dev/null`; - + my @attr_all = split /\n+/, $lsattr; - + my %attr = (); - + my ($a, $p, @f); - + foreach (@attr_all) { ($a, $p) = split(/\s/, $_, 2); @f = split /\//, $p; $attr{$f[-1]} = $a; } - + my %dirfiles = (); - + my ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks ); - + my $count = 0; - + chdir($datadir); - + while (my $item = readdir(DIR)) { #skip the . and .. special dirs @@ -1733,7 +1904,7 @@ sub dirlistfm $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks ) = stat($item); - + if(defined $uid) { $uid = getpwuid($uid); @@ -1742,7 +1913,7 @@ sub dirlistfm { $uid = ''; } - + if(defined $gid) { $gid = getgrgid($gid); @@ -1751,8 +1922,8 @@ sub dirlistfm { $gid = ''; } - - #This if else logic determines what it is, File, Directory, other + + #This if else logic determines what it is, File, Directory, other if (-T $item) { # print "File\n"; @@ -1792,13 +1963,13 @@ sub dirlistfm $count++; } closedir(DIR); - + if ($count eq 0) { logger "Empty directory $datadir."; return 1; } - + chdir AGENT_RUN_DIR; #Now we return it to the webpage, as array return {%dirfiles}; @@ -1818,7 +1989,7 @@ sub readfile close(BLANK); } } - + if (!open(USERFILE, '<', $userfile)) { logger "ERROR - Can't open file $userfile for reading."; @@ -1832,12 +2003,12 @@ sub readfile $wholefile .= encode_base64($buf); } close(USERFILE); - + if(!defined $wholefile) { return "1; "; } - + return "1;" . $wholefile; } @@ -1949,7 +2120,7 @@ sub start_file_download return -2; } } - + my $download_file_path = Path::Class::File->new($destination, "$filename"); my $pid = fork(); @@ -1958,7 +2129,7 @@ sub start_file_download logger "Could not allocate resources for download."; return -3; } - + # Only the forked child goes here. elsif ($pid == 0) { @@ -1966,11 +2137,11 @@ sub start_file_download SSL_verify_mode => 0x00 } ); $ua->agent('Mozilla/5.0'); my $response = $ua->get($url, ':content_file' => "$download_file_path"); - + if ($response->is_success) { logger "Successfully fetched $url and stored it to $download_file_path. Retval: ".$response->status_line; - + if (!-e $download_file_path) { logger "File $download_file_path does not exist."; @@ -1983,7 +2154,7 @@ sub start_file_download uncompress_file_without_decrypt($download_file_path, $destination); } - + # Run post scripts if any if ($post_script ne "") { @@ -1997,7 +2168,7 @@ sub start_file_download if(handle_lock_command_line($line) == 0){ print FILE "$line\n"; }else{ - logger "Lock command completed successfully"; + logger "Lock command completed successfully"; } } print FILE "rm -f $destination/postinstall.sh\n"; @@ -2033,9 +2204,9 @@ sub lock_additional_files{ sub lock_additional_files_logic{ my ($homedir, $filesToLock, $action, $returnType) = @_; - + logger "Locking additional files specified in the XML."; - + my $commandStr = ""; $filesToLock = startup_comma_format_to_multiline($filesToLock); $filesToLock = replace_OGP_Env_Vars("", "", $homedir, $filesToLock); @@ -2060,11 +2231,11 @@ sub lock_additional_files_logic{ } } } - + if($commandStr ne ""){ return $commandStr; } - + return ""; } @@ -2072,11 +2243,11 @@ sub run_before_start_commands { #return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($server_id, $homedir, $beforestartcmd, $pathowner) = @_; - + if ($homedir ne "" && $server_id ne ""){ # Run any prestart scripts if (defined $beforestartcmd && $beforestartcmd ne "") - { + { logger "Running pre-start XML commands before starting server ID $server_id with a home directory of $homedir."; my @prestartcmdlines = split /[\r\n]+/, $beforestartcmd; my $prestartcmdfile = $homedir."/".'prestart_ogp.sh'; @@ -2090,11 +2261,11 @@ sub run_before_start_commands close FILE; chmod 0755, $prestartcmdfile; sudo_exec_without_decrypt("bash $prestartcmdfile", $pathowner); - } + } }else{ return -2; } - + return 1; } @@ -2119,7 +2290,7 @@ sub startup_comma_format_to_multiline{ } sub create_secure_script -{ +{ my ($home_path, $exec_folder_path, $exec_path) = @_; secure_path_without_decrypt('chattr-i', $home_path); my $secure = "$home_path/secure.sh"; @@ -2143,7 +2314,7 @@ sub create_secure_script sub check_b4_chdir { my ($path, $owner) = @_; - + if (!-e $path) { logger "$path does not exist yet. Trying to create it..."; @@ -2154,45 +2325,45 @@ sub check_b4_chdir { sudo_exec_without_decrypt('mkdir -p ' . $path); } - + if (!-e $path) { return -1; } } - + my $group = SERVER_RUNNER_USER; - + if(defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ $group = `whoami`; chomp $group; } - + set_path_ownership($owner, $group, $path); - + if (!chdir $path) { logger "Unable to change dir to '$path'."; return -1; } - + return 0; } sub set_path_ownership { my ($owner, $group, $path, $skipChattr) = @_; - + my $owner_uid = `id -u $owner`; chomp $owner_uid; my $group_uid = `id -g $group`; chomp $group_uid; - + # Remove immutable flag recursivelly if(!$skipChattr){ secure_path_without_decrypt('chattr-i', $path); } - + # Set owner and perms on it recursivelly as well my $chownCommand = "chown -Rf $owner_uid:$group_uid '$path'"; my $chmodCommand = "chmod -Rf ug+rwx '$path'"; @@ -2200,27 +2371,27 @@ sub set_path_ownership sudo_exec_without_decrypt($chownCommand); sudo_exec_without_decrypt($chmodCommand); sudo_exec_without_decrypt($chmodCommandDir); - + my $groupCommand = "find '$path' -type d | xargs chmod g+s"; sudo_exec_without_decrypt($groupCommand); - + $groupCommand = "find '$path' -type d | xargs setfacl -d -m u::rwX,g::rwX,o::rx"; sudo_exec_without_decrypt($groupCommand); - + # Remove perms for other users $chmodCommand = "chmod -Rf o-rwx `find '$path' -type f`"; sudo_exec_without_decrypt($chmodCommand); - + return 0; } sub create_bash_scripts { my ( $home_path, $bash_scripts_path, $precmd, $postcmd, @installcmds ) = @_; - + $home_path =~ s/('+)/'\"$1\"'/g; $bash_scripts_path =~ s/('+)/'\"$1\"'/g; - + my @precmdlines = split /[\r\n]+/, $precmd; my $precmdfile = 'preinstall.sh'; open FILE, '>', $precmdfile; @@ -2230,7 +2401,7 @@ sub create_bash_scripts } close FILE; chmod 0755, $precmdfile; - + my @postcmdlines = split /[\r\n]+/, $postcmd; my $postcmdfile = 'postinstall.sh'; open FILE, '>', $postcmdfile; @@ -2247,7 +2418,7 @@ sub create_bash_scripts "rm -f runinstall.sh\n"; close FILE; chmod 0755, $postcmdfile; - + my $installfile = 'runinstall.sh'; open FILE, '>', $installfile; print FILE "#!/bin/bash\n". @@ -2262,7 +2433,7 @@ sub create_bash_scripts "./$postcmdfile\n"; close FILE; chmod 0755, $installfile; - + return $installfile; } @@ -2273,27 +2444,27 @@ sub start_rsync_install { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($home_id, $home_path, $url, $exec_folder_path, $exec_path, $precmd, $postcmd, $filesToLockUnlock) = decrypt_params(@_); - + my $owner = get_path_owner($home_path); - + if ( check_b4_chdir($home_path, $owner) != 0) { return 0; } - + create_secure_script($home_path, $exec_folder_path, $exec_path); - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path, $owner) != 0) { return 0; } - + # Rsync install require the rsync binary to exist in the system # to enable this functionality. my $rsync_binary = Path::Class::File->new("/usr/bin", "rsync"); - + if (!-f $rsync_binary) { logger "Failed to start rsync update from " @@ -2303,13 +2474,13 @@ sub start_rsync_install } my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); - + if(defined $filesToLockUnlock && $filesToLockUnlock ne ""){ $postcmd .= "\n" . lock_additional_files_logic($home_path, $filesToLockUnlock, "lock", "str"); } - + backup_home_log( $home_id, $log_file ); my $path = $home_path; $path =~ s/('+)/'\"$1\"'/g; @@ -2319,7 +2490,7 @@ sub start_rsync_install my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); logger "Running rsync update: /usr/bin/rsync --log-file='" . AGENT_RSYNC_GENERIC_LOG . "' --archive --compress --copy-links --update --verbose rsync://$url '$home_path'"; system($screen_cmd); - + chdir AGENT_RUN_DIR; return 1; } @@ -2333,36 +2504,36 @@ sub master_server_update return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($home_id,$home_path,$ms_home_id,$ms_home_path,$exec_folder_path,$exec_path,$precmd,$postcmd) = decrypt_params(@_); my $owner = get_path_owner($home_path); - + if ( check_b4_chdir($home_path, $owner) != 0) { return 0; } - + create_secure_script($home_path, $exec_folder_path, $exec_path); - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path, $owner) != 0) { return 0; } my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); - + backup_home_log( $home_id, $log_file ); - + my $my_home_path = $home_path; $my_home_path =~ s/('+)/'\"$1\"'/g; $exec_path =~ s/\Q$home_path\E//g; $exec_path =~ s/^\///g; $exec_path =~ s/('+)/'\"$1\"'/g; $ms_home_path =~ s/('+)/'\"$1\"'/g; - + my @installcmds = ("cd '$ms_home_path'"); - + ## Copy files that match the extensions listed at extPatterns.txt open(EXT_PATTERNS, '<', Path::Class::File->new(AGENT_RUN_DIR, "extPatterns.txt")) || logger "Error reading patterns file $!"; @@ -2373,19 +2544,19 @@ sub master_server_update push (@installcmds, "find -iname \\\*.$patern -exec cp -Rfp --parents {} '$my_home_path'/ \\\;"); } close EXT_PATTERNS; - + ## Copy the server executable so it can be secured with chattr +i push (@installcmds, "cp -vf --parents '$exec_path' '$my_home_path'"); - + ## Do symlinks for each of the other files push (@installcmds, "cp -vuRfs '$ms_home_path'/* '$my_home_path'"); - + my $installfile = create_bash_scripts( $home_path, $bash_scripts_path, $precmd, $postcmd, @installcmds ); my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); logger "Running master server update from home ID $home_id to home ID $ms_home_id"; system($screen_cmd); - + chdir AGENT_RUN_DIR; return 1; } @@ -2404,21 +2575,21 @@ sub steam_cmd_without_decrypt { my ($home_id, $home_path, $mod, $modname, $betaname, $betapwd, $user, $pass, $guard, $exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $filesToLockUnlock, $arch_bits) = @_; my $owner = get_path_owner($home_path); - + if ( check_b4_chdir($home_path, $owner) != 0) { return 0; } - + create_secure_script($home_path, $exec_folder_path, $exec_path); - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path, $owner) != 0) { return 0; } - + my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); my $screen_id_for_txt_update = substr ($screen_id, rindex($screen_id, '_') + 1); my $steam_binary = Path::Class::File->new(STEAMCMD_CLIENT_DIR, "steamcmd.sh"); @@ -2432,18 +2603,18 @@ sub steam_cmd_without_decrypt { print FILE "\@sSteamCmdForcePlatformType windows\n"; } - + # Handle requested SteamCMD architecture if(defined $arch_bits && $arch_bits ne ""){ print FILE "\@sSteamCmdForcePlatformBitness " . $arch_bits . "\n"; } - + if(defined STEAM_DL_LIMIT && STEAM_DL_LIMIT ne "" && is_integer(STEAM_DL_LIMIT) && STEAM_DL_LIMIT > 0){ print FILE "set_download_throttle " . STEAM_DL_LIMIT . "\n"; } - + print FILE "force_install_dir \"$home_path\"\n"; - + if($guard ne '') { print FILE "set_steam_guard_code $guard\n"; @@ -2456,11 +2627,11 @@ sub steam_cmd_without_decrypt { print FILE "login anonymous\n"; } - + if($modname ne "") { print FILE "app_set_config $mod mod $modname\n"; - print FILE "app_update $mod mod $modname validate\n"; + print FILE "app_update $mod mod $modname validate\n"; } if($betaname ne "" && $betapwd ne "") @@ -2475,25 +2646,25 @@ sub steam_cmd_without_decrypt { print FILE "app_update $mod\n"; } - + print FILE "exit\n"; close FILE; - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log( $home_id, $log_file ); - + my $postcmd_mod = $postcmd; - + if(defined $filesToLockUnlock && $filesToLockUnlock ne ""){ $postcmd_mod .= "\n" . lock_additional_files_logic($home_path, $filesToLockUnlock, "lock", "str"); } - + my @installcmds = ("$steam_binary +runscript $installtxt +exit"); - + my $installfile = create_bash_scripts( $home_path, $bash_scripts_path, $precmd, $postcmd_mod, @installcmds ); - + my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); - + logger "Running steam update: $steam_binary +runscript $installtxt +exit"; system($screen_cmd); @@ -2669,14 +2840,14 @@ sub uncompress_file_without_decrypt } my $filesize = (stat($file))[7]; - + if($filesize >= 3221225472 && $file =~ /\.zip$/i){ # Archive::Extract seems to have problems with large zip files, so for files greater than 3GB in size, let the system handle it logger "Using system call to unzip."; system("unzip -o $file -d $destination"); if($? != 0){ logger "Done."; - return -1; + return -1; } }else{ @@ -2698,7 +2869,7 @@ sub uncompress_file_without_decrypt logger "File uncompressed/extracted successfully."; } - + return 1; } @@ -2719,7 +2890,7 @@ sub compress_files_without_decrypt logger "compress_files: Destination path ( $destination ) could not be found."; return -1; } - + chdir $destination; my @items = split /\Q\n/, $files; my @inventory; @@ -2737,7 +2908,7 @@ sub compress_files_without_decrypt elsif (-d $item) { $zip->addTree( $item, $item ); - } + } } } # Save the file @@ -2764,7 +2935,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2791,7 +2962,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2818,7 +2989,7 @@ sub compress_files_without_decrypt @inventory = (); find (sub { push @inventory, $File::Find::name }, $item); $tar->add_files( @inventory ); - } + } } } # Save the file @@ -2914,7 +3085,7 @@ sub mon_stats # Collect all resource statistics logger "Collecting resource usage statistics..."; - + # Get CPU usage my %cpu_data; my %prev_idle; @@ -2950,7 +3121,7 @@ sub mon_stats $cpu_core_count++; } my $avg_cpu_usage = $cpu_core_count > 0 ? sprintf("%.2f", $total_cpu_usage / $cpu_core_count) : 0; - + # Get RAM usage my($mem_total, $buffers, $cached, $mem_free) = qw(0 0 0 0); open(STAT, '/proc/meminfo'); @@ -2965,14 +3136,14 @@ sub mon_stats my $mem_percent = $mem_total > 0 ? sprintf("%.2f", 100 * $mem_used / $mem_total) : 0; $mem_total *= 1024; # Convert to bytes $mem_used *= 1024; # Convert to bytes - + # Get disk usage my($disk_total, $disk_used, $disk_free) = split(' ', `df -lP 2>/dev/null|grep "^/dev/.*"|awk '{total+=\$2}{used+=\$3}{free+=\$4} END {print total, used, free}'`); my $disk_percent = $disk_total > 0 ? sprintf("%.2f", 100 * $disk_used / $disk_total) : 0; $disk_total *= 1024; # Convert to bytes $disk_used *= 1024; # Convert to bytes $disk_free *= 1024; # Convert to bytes - + # Get uptime open(STAT, '/proc/uptime'); my $uptime = 0; @@ -2980,7 +3151,7 @@ sub mon_stats $uptime += $1 if /^([0-9]+)/; } close STAT; - + # Get load average my $load_avg_1min = "0"; my $load_avg_5min = "0"; @@ -2994,13 +3165,13 @@ sub mon_stats } } close LOADAVG; - + # Log the collected statistics logger "Resource stats collected - CPU: ${avg_cpu_usage}%, Memory: ${mem_percent}% (${mem_used}/${mem_total} bytes), Disk: ${disk_percent}% (${disk_used}/${disk_total} bytes), Uptime: ${uptime}s, Load: ${load_avg_1min}/${load_avg_5min}/${load_avg_15min}"; - + # Submit to database if configured my $submit_result = submit_resource_stats_to_db($avg_cpu_usage, $mem_used, $mem_total, $mem_percent, $disk_used, $disk_total, $disk_free, $disk_percent, $uptime, $load_avg_1min, $load_avg_5min, $load_avg_15min); - + if ($submit_result == 1) { logger "Resource statistics successfully submitted to MySQL database."; } elsif ($submit_result == -1) { @@ -3020,9 +3191,9 @@ sub mon_stats sub submit_resource_stats_to_db { my ($cpu_usage, $mem_used, $mem_total, $mem_percent, $disk_used, $disk_total, $disk_free, $disk_percent, $uptime, $load_1min, $load_5min, $load_15min) = @_; - + # Check if database is configured - if (!defined STATS_DB_HOST || STATS_DB_HOST eq '' || + if (!defined STATS_DB_HOST || STATS_DB_HOST eq '' || !defined STATS_DB_USER || STATS_DB_USER eq '' || !defined STATS_DB_PASS || STATS_DB_PASS eq '' || STATS_DB_PASS eq 'REPLACE_ME' || !defined STATS_DB_NAME || STATS_DB_NAME eq '') { @@ -3030,7 +3201,7 @@ sub submit_resource_stats_to_db scheduler_log_events("Resource stats database not configured - skipping submission"); return -1; } - + my $dbh; eval { # Connect to MySQL database @@ -3041,43 +3212,43 @@ sub submit_resource_stats_to_db AutoCommit => 1, mysql_enable_utf8 => 1 }); - + if (!$dbh) { logger "Failed to connect to MySQL database: $DBI::errstr"; return 0; } - + logger "Successfully connected to MySQL database for resource stats submission."; - + # Create the proper database tables based on the schema files create_resource_stats_tables($dbh); - + # Get machine information my $machine_id = get_machine_id(); my $hostname = `hostname -f 2>/dev/null || hostname`; chomp($hostname); my $ip = get_local_ip(); - + # Ensure the machine is registered ensure_machine_registered($dbh, $machine_id, $hostname, $ip); - + # Get additional system metrics for proper schema compliance my ($swap_used, $swap_total, $disk_path, $net_iface, $rx_bytes, $tx_bytes, $iface_speed) = get_extended_system_metrics(); - + # Insert machine-level resource sample - insert_machine_sample($dbh, $machine_id, $cpu_usage, $mem_used, $mem_total, $mem_percent, + insert_machine_sample($dbh, $machine_id, $cpu_usage, $mem_used, $mem_total, $mem_percent, $swap_used, $swap_total, $disk_path, $disk_total, $disk_used, $disk_percent, $net_iface, $rx_bytes, $tx_bytes, $iface_speed, $load_1min, $load_5min, $load_15min); - + # Collect and insert per-process/server resource samples collect_and_insert_process_samples($dbh, $machine_id); - + $dbh->disconnect(); - + logger "Resource statistics inserted into database successfully."; return 1; }; - + if ($@) { logger "Error submitting resource stats to database: $@"; if ($dbh) { @@ -3085,7 +3256,7 @@ sub submit_resource_stats_to_db } return 0; } - + return 1; } @@ -3093,7 +3264,7 @@ sub create_resource_stats_tables { my ($dbh) = @_; my $table_prefix = STATS_TABLE_PREFIX || 'gsp_'; - + # Create gsp_machines table my $machines_sql = qq{ CREATE TABLE IF NOT EXISTS `${table_prefix}machines` ( @@ -3106,7 +3277,7 @@ sub create_resource_stats_tables UNIQUE KEY `uniq_machine` (`machine_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci }; - + # Create gsp_machine_samples table my $machine_samples_sql = qq{ CREATE TABLE IF NOT EXISTS `${table_prefix}machine_samples` ( @@ -3135,7 +3306,7 @@ sub create_resource_stats_tables KEY `idx_ts` (`ts`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci }; - + # Create gsp_process_samples table my $process_samples_sql = qq{ CREATE TABLE IF NOT EXISTS `${table_prefix}process_samples` ( @@ -3162,7 +3333,7 @@ sub create_resource_stats_tables KEY `idx_ts` (`ts`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci }; - + logger "Creating resource stats database tables..."; $dbh->do($machines_sql); $dbh->do($machine_samples_sql); @@ -3175,7 +3346,7 @@ sub get_machine_id # Use hostname as machine identifier for better identification my $machine_id = `hostname`; chomp($machine_id); - + # Fallback to agent key only if hostname is not available if (!$machine_id || $machine_id eq '') { $machine_id = AGENT_KEY; @@ -3199,14 +3370,14 @@ sub ensure_machine_registered { my ($dbh, $machine_id, $hostname, $ip) = @_; my $table_prefix = STATS_TABLE_PREFIX || 'gsp_'; - + # Check if machine exists, if not insert it my $check_sql = "SELECT id FROM `${table_prefix}machines` WHERE machine_id = ?"; my $sth = $dbh->prepare($check_sql); $sth->execute($machine_id); my $result = $sth->fetchrow_hashref(); $sth->finish(); - + if (!$result) { my $insert_sql = "INSERT INTO `${table_prefix}machines` (machine_id, hostname, ip) VALUES (?, ?, ?)"; my $insert_sth = $dbh->prepare($insert_sql); @@ -3227,15 +3398,15 @@ sub get_extended_system_metrics } close(MEMINFO); } - + # Get disk path (root filesystem) my $disk_path = '/'; - + # Get network interface and statistics my ($net_iface, $rx_bytes, $tx_bytes, $iface_speed) = ('', 0, 0, 0); my $default_iface = `ip route | grep default | head -1 | awk '{print \$5}'`; chomp($default_iface); - + if ($default_iface) { $net_iface = $default_iface; if (open(NETDEV, '/proc/net/dev')) { @@ -3248,7 +3419,7 @@ sub get_extended_system_metrics } close(NETDEV); } - + # Try to get interface speed my $speed_file = "/sys/class/net/$default_iface/speed"; if (open(SPEED, $speed_file)) { @@ -3258,18 +3429,18 @@ sub get_extended_system_metrics close(SPEED); } } - + return ($swap_used, $swap_total, $disk_path, $net_iface, $rx_bytes, $tx_bytes, $iface_speed); } sub insert_machine_sample { - my ($dbh, $machine_id, $cpu_usage, $mem_used, $mem_total, $mem_percent, + my ($dbh, $machine_id, $cpu_usage, $mem_used, $mem_total, $mem_percent, $swap_used, $swap_total, $disk_path, $disk_total, $disk_used, $disk_percent, $net_iface, $rx_bytes, $tx_bytes, $iface_speed, $load_1min, $load_5min, $load_15min) = @_; - + my $table_prefix = STATS_TABLE_PREFIX || 'gsp_'; - + my $insert_sql = qq{ INSERT INTO `${table_prefix}machine_samples` ( machine_id, ts, load1, load5, load15, cpu_pct, mem_used_bytes, mem_total_bytes, mem_used_pct, @@ -3277,7 +3448,7 @@ sub insert_machine_sample net_iface, rx_bytes, tx_bytes, iface_speed_mbps ) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) }; - + my $sth = $dbh->prepare($insert_sql); $sth->execute( $machine_id, $load_1min, $load_5min, $load_15min, $cpu_usage, $mem_used, $mem_total, $mem_percent, @@ -3285,7 +3456,7 @@ sub insert_machine_sample $net_iface, $rx_bytes, $tx_bytes, $iface_speed ); $sth->finish(); - + logger "Machine sample inserted for $machine_id: CPU=$cpu_usage%, MEM=$mem_percent%, DISK=$disk_percent%"; } @@ -3293,16 +3464,16 @@ sub collect_and_insert_process_samples { my ($dbh, $machine_id) = @_; my $table_prefix = STATS_TABLE_PREFIX || 'gsp_'; - + # Get list of running game server processes (screen sessions and their child processes) my @processes = (); - + # Create lookup table for home_id to server_id mapping from startup files my %home_id_to_server_id = (); if (opendir(STARTUP_DIR_EARLY, GAME_STARTUP_DIR)) { while (my $startup_file = readdir(STARTUP_DIR_EARLY)) { next if $startup_file =~ /^\./; - + my $startup_path = Path::Class::File->new(GAME_STARTUP_DIR, $startup_file); if (open(STARTUP_FILE_EARLY, '<', $startup_path)) { while () { @@ -3322,15 +3493,15 @@ sub collect_and_insert_process_samples # Find screen sessions that might be game servers my @screen_sessions = `screen -ls 2>/dev/null | grep -E "\\s+[0-9]+\\." | awk '{print \$1}'`; - + foreach my $session (@screen_sessions) { chomp($session); next unless $session; - + # Extract server name from screen session name (format: OGP_HOME_000001518 or similar) my $server_name = $session; my $server_path = '/'; - + # Try to extract home_id from OGP screen session and map to server_id if ($session =~ /OGP_HOME_0*(\d+)/) { my $home_id = $1; @@ -3344,33 +3515,33 @@ sub collect_and_insert_process_samples } elsif ($session =~ /(\d+)\.(.+)/) { $server_name = $2; } - + # Get PIDs of processes in this screen session my $screen_pid_line = `screen -ls 2>/dev/null | grep '$session'`; my ($screen_pid) = $screen_pid_line =~ /(\d+)\./; - + if ($screen_pid && $screen_pid =~ /^\d+$/) { # Get child processes of the screen session my @child_pids = `pgrep -P $screen_pid 2>/dev/null`; push @child_pids, $screen_pid; # Include the screen process itself - + foreach my $pid (@child_pids) { chomp($pid); next unless $pid && $pid =~ /^\d+$/; - + # Get process information my $proc_info = get_process_info($pid, $server_name, $server_path); push @processes, $proc_info if $proc_info; } } } - + # Check startup files to find configured game server executables my %startup_executables = (); if (opendir(STARTUP_DIR, GAME_STARTUP_DIR)) { while (my $startup_file = readdir(STARTUP_DIR)) { next if $startup_file =~ /^\./; - + my $startup_path = Path::Class::File->new(GAME_STARTUP_DIR, $startup_file); if (open(STARTUP_FILE, '<', $startup_path)) { while () { @@ -3391,11 +3562,11 @@ sub collect_and_insert_process_samples } closedir(STARTUP_DIR); } - + my $startup_exe_count = scalar(keys %startup_executables); logger "Found $startup_exe_count executable(s) from startup files for process monitoring" if $startup_exe_count > 0; logger "Resource monitoring will ONLY track processes that have corresponding startup files in the startups folder" if $startup_exe_count > 0; - + # Only look for processes that have corresponding startup files (managed game servers) # This prevents monitoring orphaned processes and unrelated services foreach my $exe_name (keys %startup_executables) { @@ -3403,24 +3574,24 @@ sub collect_and_insert_process_samples foreach my $pid (@pids) { chomp($pid); next unless $pid && $pid =~ /^\d+$/; - + # Avoid duplicates by checking if we already processed this PID next if grep { $_->{pid} == $pid } @processes; - + # Only process if this executable has a startup file (is managed by OGP) if (exists $startup_executables{$exe_name}) { my $startup_info = $startup_executables{$exe_name}; # Use the actual server ID from the startup file my $server_name = $startup_info->{server_id} || "Unknown_Server_PID$pid"; my $server_path = $startup_info->{home_path} || '/'; - + my $proc_info = get_process_info($pid, $server_name, $server_path); push @processes, $proc_info if $proc_info; } # If no startup file exists for this executable, we skip it (not managed by OGP) } } - + # Insert process samples into database my $insert_sql = qq{ INSERT INTO `${table_prefix}process_samples` ( @@ -3428,10 +3599,10 @@ sub collect_and_insert_process_samples mem_pct, io_read_bytes, io_write_bytes, open_fds, listening_ports, folder_size_bytes ) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) }; - + my $sth = $dbh->prepare($insert_sql); my $samples_inserted = 0; - + foreach my $proc (@processes) { eval { $sth->execute( @@ -3446,7 +3617,7 @@ sub collect_and_insert_process_samples logger "Error inserting process sample for PID $proc->{pid}: $@"; } } - + $sth->finish(); logger "Process samples inserted: $samples_inserted processes for $machine_id"; } @@ -3454,23 +3625,23 @@ sub collect_and_insert_process_samples sub get_process_info { my ($pid, $server_name, $server_path) = @_; - + # Get process stats from /proc//stat my $stat_file = "/proc/$pid/stat"; return undef unless -r $stat_file; - + my $stat_line = `cat $stat_file 2>/dev/null`; chomp($stat_line); return undef unless $stat_line; - + my @stat_fields = split /\s+/, $stat_line; my $proc_name = $stat_fields[1] || ''; $proc_name =~ s/[()]//g; # Remove parentheses - + # Get command line my $cmd = `cat /proc/$pid/cmdline 2>/dev/null | tr '\\0' ' '`; chomp($cmd); - + # Get memory info from /proc//status my ($rss_bytes, $vms_bytes) = (0, 0); if (open(STATUS, "/proc/$pid/status")) { @@ -3480,14 +3651,14 @@ sub get_process_info } close(STATUS); } - + # Get CPU percentage (using utime + stime from /proc/pid/stat) my $cpu_pct = 0; if (@stat_fields >= 15) { my $utime = $stat_fields[13] || 0; # user time my $stime = $stat_fields[14] || 0; # system time my $total_time = $utime + $stime; - + # Get system uptime and process start time for CPU calculation my $uptime_info = `cat /proc/uptime 2>/dev/null`; if ($uptime_info && $uptime_info =~ /^(\S+)/) { @@ -3495,14 +3666,14 @@ sub get_process_info my $starttime = $stat_fields[21] || 0; # process start time in clock ticks my $hertz = `getconf CLK_TCK 2>/dev/null` || 100; # clock ticks per second chomp($hertz); - + my $process_uptime = $system_uptime - ($starttime / $hertz); if ($process_uptime > 0) { $cpu_pct = sprintf("%.2f", ($total_time / $hertz) / $process_uptime * 100); } } } - + # Calculate memory percentage (of total system memory) my $mem_pct = 0; my $total_mem = `grep MemTotal /proc/meminfo | awk '{print \$2}'`; @@ -3510,7 +3681,7 @@ sub get_process_info if ($total_mem && $total_mem > 0) { $mem_pct = sprintf("%.2f", ($rss_bytes / 1024) / $total_mem * 100); } - + # Get I/O stats my ($io_read_bytes, $io_write_bytes) = (0, 0); if (open(IO, "/proc/$pid/io")) { @@ -3520,7 +3691,7 @@ sub get_process_info } close(IO); } - + # Get open file descriptors count my $open_fds = 0; my $fd_dir = "/proc/$pid/fd"; @@ -3529,7 +3700,7 @@ sub get_process_info $open_fds = scalar(grep { /^\d+$/ } @fds); closedir(PROC_FD_DIR); } - + # Get listening ports (simplified) my $listening_ports = ''; my $netstat_output = `netstat -tlnp 2>/dev/null | grep '$pid/' | awk '{print \$4}' | cut -d: -f2 | sort -n | uniq`; @@ -3537,7 +3708,7 @@ sub get_process_info chomp($netstat_output); $listening_ports = join(',', split(/\n/, $netstat_output)); } - + # Get folder size (for server_path) - try to find server directory from command line my $folder_size_bytes = 0; if ($cmd && $cmd =~ m{(/\S+)}) { @@ -3552,7 +3723,7 @@ sub get_process_info $folder_size_bytes = $du_output if $du_output && $du_output =~ /^\d+$/; } } - + return { server_name => $server_name, server_path => $server_path, @@ -3628,11 +3799,11 @@ sub remove_home logger "ERROR - $home_path_del does not exist...nothing to do"; return 0; } - + my $owner = get_path_owner($home_path_del); secure_path_without_decrypt('chattr-i', $home_path_del); my $deleted_home_dir = sudo_exec_without_decrypt('rm -rf \''.$home_path_del.'\''); - + if (defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ if ($owner ne SERVER_RUNNER_USER && begins_with($owner,'gamehome')){ my $kill_all_user = sudo_exec_without_decrypt('killall -u "' . $owner . '"'); @@ -3644,14 +3815,14 @@ sub remove_home my $deleted_user_group = sudo_exec_without_decrypt('groupdel "' . $owner . '"'); } } - + my ($retval, $enc_out) = split(/;/, $deleted_home_dir, 2); if ($retval == 1){ logger "Deletetion of $home_path_del successful!"; }else{ logger "Deletetion of $home_path_del failed!"; } - + return 1; } @@ -3672,10 +3843,12 @@ sub restart_server_without_decrypt $control_password, $control_type, $home_path, $server_exe, $run_dir, $cmd, $cpu, $nice, $preStart, $envVars, $game_key, $console_log) = @_; - if (stop_server_without_decrypt($home_id, $server_ip, - $server_port, $control_protocol, - $control_password, $control_type, $home_path) == 0) + if (stop_server_without_decrypt($home_id, $server_ip, + $server_port, $control_protocol, + $control_password, $control_type, $home_path) == 0) { + logger "Waiting 60 seconds before starting the server again."; + sleep 60; if (universal_start_without_decrypt($home_id, $home_path, $server_exe, $run_dir, $cmd, $server_port, $server_ip, $cpu, $nice, $preStart, $envVars, $game_key, $console_log) == 1) { @@ -3695,23 +3868,23 @@ sub restart_server_without_decrypt sub find_user_by_screen_id { my ($screen_id) = @_; - + my $screen_user = SERVER_RUNNER_USER; - + if(defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ $screen_user = `whoami`; chomp $screen_user; } - + my $ret = sudo_exec_without_decrypt('find /var/run/screen -name "*'.$screen_id.'"'); - + my ($retval, $enc_out) = split(/;/, $ret, 2); - + if($retval != 1) { return $screen_user; } - + if( defined($enc_out) && $enc_out =~ /^(.+)\\n/ ) { my @dec_out = (); @@ -3719,9 +3892,9 @@ sub find_user_by_screen_id my $dec_line = decode_base64($line); push @dec_out, $dec_line; } - + my @path_parts = split /\//, $dec_out[0]; - + if ($#path_parts == 5) { if($path_parts[5] =~ /^(\d+)\.$screen_id$/) @@ -3739,32 +3912,32 @@ sub find_user_by_screen_id } } } - + return $screen_user; } sub get_path_owner { my ($path) = @_; - + my $path_owner = SERVER_RUNNER_USER; - + if(defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ $path_owner = `whoami`; chomp $path_owner; } - + if(-d $path) { my $ret = sudo_exec_without_decrypt('stat -c "%U" "'.$path.'"'); - + my ($retval, $enc_out) = split(/;/, $ret, 2); - + if($retval != 1) { return $path_owner; } - + if( defined($enc_out) && $enc_out =~ /^(.+)\\n/ ) { my @dec_out = (); @@ -3775,14 +3948,14 @@ sub get_path_owner my $parseval = $dec_out[0]; my $uid = `id -u $parseval`; - + if( $uid =~ /^(\d+)$/ ) { $path_owner = $parseval; } } } - + return $path_owner; } @@ -3801,20 +3974,20 @@ sub sudo_exec_without_decrypt { $as_user = "root"; } - + my $command = "echo '$SUDOPASSWD'|sudo -kS -p \"\" su -c '$sudo_exec;echo \$?' $as_user 2>&1"; my @cmdret = qx($command); $cmdret[0] =~ s/^//g if defined $cmdret[0]; chomp(@cmdret); - + my $ret = pop(@cmdret); chomp($ret); - + if ("X$ret" eq "X0") { return "1;".encode_list(@cmdret); } - + return -1; } @@ -3826,20 +3999,20 @@ sub sudo_exec_without_decrypt_no_return { $as_user = "root"; } - + my $command = "echo '$SUDOPASSWD'|sudo -kS -p \"\" su -c '$sudo_exec' $as_user 2>&1"; my @cmdret = qx($command); $cmdret[0] =~ s/^//g if defined $cmdret[0]; chomp(@cmdret); - + my $ret = pop(@cmdret); chomp($ret); - + if ("X$ret" eq "X0") { return "1;".encode_list(@cmdret); } - + return -1; } @@ -3851,20 +4024,20 @@ sub secure_path } sub secure_path_without_decrypt -{ +{ my ($action, $file_path, $returnType) = @_; my $checkIfFileExists = 1; - + if(defined $returnType && $returnType eq "str"){ $checkIfFileExists = 0; } - + if($checkIfFileExists){ if(! -e $file_path){ return -1; } } - + $file_path =~ s/('+)/'\"$1\"'/g; if($action eq "chattr+i") { @@ -3882,12 +4055,12 @@ sub secure_path_without_decrypt return sudo_exec_without_decrypt('chattr -Rf -i \''.$file_path.'\''); } } - + return -1; } sub get_chattr -{ +{ return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($file_path) = decrypt_params(@_); my $file = $file_path; @@ -3899,30 +4072,30 @@ sub ftp_mgr { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($action, $login, $password, $home_path) = decrypt_params(@_); - + my $uid = `id -u`; chomp $uid; my $gid = `id -g`; chomp $gid; - + $login =~ s/('+)/'\"$1\"'/g; $password =~ s/('+)/'\"$1\"'/g; $home_path =~ s/('+)/'\"$1\"'/g; - + if(!defined($Cfg::Preferences{ogp_manages_ftp}) || (defined($Cfg::Preferences{ogp_manages_ftp}) && $Cfg::Preferences{ogp_manages_ftp} eq "1")){ if( defined($Cfg::Preferences{ftp_method}) && $Cfg::Preferences{ftp_method} eq "IspConfig") { use constant ISPCONFIG_DIR => Path::Class::Dir->new(AGENT_RUN_DIR, 'IspConfig'); use constant FTP_USERS_DIR => Path::Class::Dir->new(ISPCONFIG_DIR, 'ftp_users'); - + if (!-d FTP_USERS_DIR && !mkdir FTP_USERS_DIR) { print "Could not create " . FTP_USERS_DIR . " directory $!."; return -1; } - + chdir ISPCONFIG_DIR; - + if($action eq "list") { my $users_list; @@ -3968,15 +4141,15 @@ sub ftp_mgr my $phpScript; my $phpOut; my $gidTwo = SERVER_RUNNER_USER; - + chmod 0777, 'ehcp_ftp_log.txt'; - + # In order to access the FTP files, the vsftpd user needs to be added to the ogp group - sudo_exec_without_decrypt("usermod -a -G '$gid' ftp"); - sudo_exec_without_decrypt("usermod -a -G '$gid' vsftpd"); - sudo_exec_without_decrypt("usermod -a -G '$gidTwo' ftp"); - sudo_exec_without_decrypt("usermod -a -G '$gidTwo' vsftpd"); - + sudo_exec_without_decrypt("usermod -a -G '$gid' ftp"); + sudo_exec_without_decrypt("usermod -a -G '$gid' vsftpd"); + sudo_exec_without_decrypt("usermod -a -G '$gidTwo' ftp"); + sudo_exec_without_decrypt("usermod -a -G '$gidTwo' vsftpd"); + if($action eq "list") { return "1;".encode_list(`php-cgi -f listAllUsers.php`); @@ -3991,13 +4164,13 @@ sub ftp_mgr { $phpScript = `php-cgi -f addAccount.php username=\'$login\' password=\'$password\' dir=\'$home_path\' uid=$uid gid=$gid`; $phpOut = `php-cgi -f syncftp.php`; - return $phpScript; + return $phpScript; } elsif($action eq "passwd") { $phpScript = `php-cgi -f updatePass.php username=\'$login\' password=\'$password\'`; $phpOut = `php-cgi -f syncftp.php`; - return $phpScript ; + return $phpScript ; } elsif($action eq "show") { @@ -4072,18 +4245,18 @@ sub ftp_mgr elsif($action eq "usermod") { my $update_account = "pure-pw usermod '$login' -u $uid -g $gid"; - + my @account_settings = split /[\n]+/, $password; - + foreach my $setting (@account_settings) { my ($key, $value) = split /[\t]+/, $setting; - + if( $key eq 'Directory' ) { $value =~ s/('+)/'\"$1\"'/g; $update_account .= " -d '$value'"; } - + if( $key eq 'Full_name' ) { if( $value ne "" ) @@ -4096,7 +4269,7 @@ sub ftp_mgr $update_account .= ' -c ""'; } } - + if( $key eq 'Download_bandwidth' && $value ne "" ) { my $Download_bandwidth; @@ -4110,7 +4283,7 @@ sub ftp_mgr } $update_account .= " -t " . $Download_bandwidth; } - + if( $key eq 'Upload___bandwidth' && $value ne "" ) { my $Upload___bandwidth; @@ -4124,7 +4297,7 @@ sub ftp_mgr } $update_account .= " -T " . $Upload___bandwidth; } - + if( $key eq 'Max_files' ) { if( $value eq "0" ) @@ -4140,7 +4313,7 @@ sub ftp_mgr $update_account .= ' -n ""'; } } - + if( $key eq 'Max_size' ) { if( $value ne "" && $value ne "0" ) @@ -4152,24 +4325,24 @@ sub ftp_mgr $update_account .= ' -N ""'; } } - + if( $key eq 'Ratio' && $value ne "" ) { my($upload_ratio,$download_ratio) = split/:/,$value; - + if($upload_ratio eq "0") { $upload_ratio = "\"\""; } $update_account .= " -q " . $upload_ratio; - + if($download_ratio eq "0") { $download_ratio = "\"\""; } $update_account .= " -Q " . $download_ratio; } - + if( $key eq 'Allowed_client_IPs' ) { if( $value ne "" ) @@ -4181,7 +4354,7 @@ sub ftp_mgr $update_account .= ' -r ""'; } } - + if( $key eq 'Denied__client_IPs' ) { if( $value ne "" ) @@ -4193,7 +4366,7 @@ sub ftp_mgr $update_account .= ' -R ""'; } } - + if( $key eq 'Allowed_local__IPs' ) { if( $value ne "" ) @@ -4205,7 +4378,7 @@ sub ftp_mgr $update_account .= ' -i ""'; } } - + if( $key eq 'Denied__local__IPs' ) { if( $value ne "" ) @@ -4217,13 +4390,13 @@ sub ftp_mgr $update_account .= ' -I ""'; } } - - + + if( $key eq 'Max_sim_sessions' && $value ne "" ) { $update_account .= " -y " . $value; } - + if ( $key eq 'Time_restrictions' ) { if( $value eq "0000-0000") @@ -4495,7 +4668,7 @@ sub agent_restart { my $init_pid = `cat ogp_agent_run.pid`; chomp($init_pid); - + if(kill 0, $init_pid) { my $or_exist = ""; @@ -4510,7 +4683,7 @@ sub agent_restart $or_exist = " -o -e /proc/$agent_pid"; } } - + open (AGENT_RESTART_SCRIPT, '>', 'tmp_restart.sh'); my $restart = "echo -n \"Stopping OGP Agent...\"\n". "kill $init_pid\n". @@ -4549,10 +4722,10 @@ sub scheduler_dispatcher { sub collect_and_submit_resource_stats { my ($task, $args) = @_; - + logger "Automated resource stats collection started."; scheduler_log_events("Resource stats collection started"); - + eval { # Collect all resource statistics # Get CPU usage @@ -4589,7 +4762,7 @@ sub collect_and_submit_resource_stats { $cpu_core_count++; } my $avg_cpu_usage = $cpu_core_count > 0 ? sprintf("%.2f", $total_cpu_usage / $cpu_core_count) : 0; - + # Get RAM usage my($mem_total, $buffers, $cached, $mem_free) = qw(0 0 0 0); open(STAT, '/proc/meminfo'); @@ -4604,14 +4777,14 @@ sub collect_and_submit_resource_stats { my $mem_percent = $mem_total > 0 ? sprintf("%.2f", 100 * $mem_used / $mem_total) : 0; $mem_total *= 1024; # Convert to bytes $mem_used *= 1024; # Convert to bytes - + # Get disk usage my($disk_total, $disk_used, $disk_free) = split(' ', `df -lP 2>/dev/null|grep "^/dev/.*"|awk '{total+=\$2}{used+=\$3}{free+=\$4} END {print total, used, free}'`); my $disk_percent = $disk_total > 0 ? sprintf("%.2f", 100 * $disk_used / $disk_total) : 0; $disk_total *= 1024; # Convert to bytes $disk_used *= 1024; # Convert to bytes $disk_free *= 1024; # Convert to bytes - + # Get uptime open(STAT, '/proc/uptime'); my $uptime = 0; @@ -4619,7 +4792,7 @@ sub collect_and_submit_resource_stats { $uptime += $1 if /^([0-9]+)/; } close STAT; - + # Get load average my $load_avg_1min = "0"; my $load_avg_5min = "0"; @@ -4633,15 +4806,15 @@ sub collect_and_submit_resource_stats { } } close LOADAVG; - + # Log the collected statistics my $stats_summary = "CPU: ${avg_cpu_usage}%, Memory: ${mem_percent}% (${mem_used}/${mem_total} bytes), Disk: ${disk_percent}% (${disk_used}/${disk_total} bytes), Uptime: ${uptime}s, Load: ${load_avg_1min}/${load_avg_5min}/${load_avg_15min}"; logger "Scheduled resource stats collection - $stats_summary"; scheduler_log_events("Resource usage collected - $stats_summary"); - + # Submit to database my $submit_result = submit_resource_stats_to_db($avg_cpu_usage, $mem_used, $mem_total, $mem_percent, $disk_used, $disk_total, $disk_free, $disk_percent, $uptime, $load_avg_1min, $load_avg_5min, $load_avg_15min); - + if ($submit_result == 1) { logger "Scheduled resource statistics successfully submitted to MySQL database."; scheduler_log_events("Resource stats successfully submitted to MySQL database"); @@ -4653,7 +4826,7 @@ sub collect_and_submit_resource_stats { scheduler_log_events("Resource stats: failed to submit to MySQL database - error occurred"); } }; - + if ($@) { logger "Error in scheduled resource stats collection: $@"; scheduler_log_events("Error in resource stats collection: $@"); @@ -4731,7 +4904,7 @@ sub scheduler_add_task print TASKS "$new_task\n"; logger "Created new task: $new_task"; close(TASKS); - scheduler_stop(); + scheduler_stop(); # Create new object with default dispatcher for scheduled tasks $cron = new Schedule::Cron( \&scheduler_dispatcher, { nofork => 1, @@ -4844,10 +5017,10 @@ sub scheduler_read_tasks # Store built-in scheduler tasks before cleaning timetable my @entries = $cron->list_entries(); my @builtin_tasks = (); - + # Only log detailed task information when there are actual user tasks to process my $user_task_count = 0; - + # Preserve built-in tasks (those not starting with 'task_') foreach my $entry (@entries) { my $is_user_task = 0; @@ -4860,27 +5033,27 @@ sub scheduler_read_tasks logger "scheduler_read_tasks: Removing user task: $first_arg"; } } - + # Keep all tasks that are NOT user-defined if (!$is_user_task) { push @builtin_tasks, $entry; } } - + # Only log when there are user tasks to process, reducing spam from built-in task preservation if ($user_task_count > 0) { logger "scheduler_read_tasks: Found " . @entries . " existing tasks, processing $user_task_count user tasks"; logger "scheduler_read_tasks: Preserving " . @builtin_tasks . " built-in tasks"; } - + # Clear the timetable $cron->clean_timetable(); - + # Re-add built-in tasks foreach my $builtin_task (@builtin_tasks) { $cron->add_entry($builtin_task->{time}, $builtin_task->{dispatcher}, @{$builtin_task->{args}}); } - + # Only log re-adding when there were user tasks processed if ($user_task_count > 0) { logger "scheduler_read_tasks: Re-added " . @builtin_tasks . " built-in tasks"; @@ -4892,11 +5065,11 @@ sub scheduler_read_tasks scheduler_stop(); return -1; } - + my $i = 0; my $task_count = 0; while () - { + { next if $_ =~ /^(#.*|[\s|\t]*?\n)/; my ($minute, $hour, $dayOfTheMonth, $month, $dayOfTheWeek, @args) = split(' ', $_); my $time = "$minute $hour $dayOfTheMonth $month $dayOfTheWeek"; @@ -4974,14 +5147,14 @@ sub get_file_part logger "ERROR - Can't open file $file for reading."; return -1; } - + binmode(FILE); - - if($offset != 0) + + if($offset != 0) { return -1 unless seek FILE, $offset, 0; } - + my $data = ""; my ($n, $buf); my $limit = $offset + 60 * 57 * 1000; #Max 3420Kb (1000 iterations) (top statistics ~ VIRT 116m, RES 47m) @@ -4990,7 +5163,7 @@ sub get_file_part $offset += $n; } close(FILE); - + if( $data ne "" ) { my $b64zlib = encode_base64(compress($data,9)); @@ -5019,7 +5192,7 @@ sub shell_action { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); my ($action, $arguments) = decrypt_params(@_); - + if($action eq 'remove_file') { chomp($arguments); @@ -5227,7 +5400,7 @@ sub remote_query chdir($php_query_dir->subdir('lgsl')); my $cmd = $PHP_CGI . " -f lgsl_feed.php" . - " lgsl_type=" . $game_type . + " lgsl_type=" . $game_type . " ip=" . $ip . " c_port=" . $c_port . " q_port=" . $q_port . @@ -5247,7 +5420,7 @@ sub remote_query chdir($php_query_dir->subdir('gameq')); my $cmd = $PHP_CGI . " -f gameq_feed.php" . - " game_type=" . $game_type . + " game_type=" . $game_type . " ip=" . $ip . " c_port=" . $c_port . " q_port=" . $q_port . @@ -5292,15 +5465,15 @@ sub steam_workshop_without_decrypt my ($home_id, $mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list, $anonymous_login, $user, $pass, $download_method, $url_list, $filename_list) = @_; - + # Creates mods path if it doesn't exist my $owner = SERVER_RUNNER_USER; - + if(defined LINUX_USER_PER_GAME_SERVER && LINUX_USER_PER_GAME_SERVER eq "1"){ $owner = `whoami`; chomp $owner; @@ -5310,7 +5483,7 @@ sub steam_workshop_without_decrypt { return -1; } - + my $secure = "$mods_full_path/secure.sh"; my $home_path = $mods_full_path; $home_path =~ s/('+)/'\"$1\"'/g; @@ -5320,15 +5493,15 @@ sub steam_workshop_without_decrypt print FILE "chmod 771 '$home_path'\n". "rm -f '$sec'"; close FILE; - + my $screen_id = create_screen_id(SCREEN_TYPE_UPDATE, $home_id); my @workshop_mods = split /,/, $mods_list; my @installcmds; - + if($download_method eq 'steamcmd') { my $steam_binary = STEAMCMD_CLIENT_BIN; - my $installSteamFile = $screen_id . "_workshop.txt"; + my $installSteamFile = $screen_id . "_workshop.txt"; my $installtxt = Path::Class::File->new(STEAMCMD_CLIENT_DIR, $installSteamFile); open FILE, '>', $installtxt; @@ -5351,7 +5524,7 @@ sub steam_workshop_without_decrypt close FILE; @installcmds = ("$steam_binary +runscript $installtxt +exit"); } - + if($download_method eq 'steamapi') { my @urls = split /,/, $url_list; @@ -5360,7 +5533,7 @@ sub steam_workshop_without_decrypt foreach my $workshop_mod_id (@workshop_mods) { my $steamcmd_download_path = '/steamapps/workshop/content/'.$workshop_id.'/'.$workshop_mod_id.'/'; - + my $workshop_mod_path = $mods_full_path.$steamcmd_download_path; if(!-d $workshop_mod_path && !mkpath $workshop_mod_path) { @@ -5375,33 +5548,33 @@ sub steam_workshop_without_decrypt $index++; } } - + my $log_file = Path::Class::File->new(SCREEN_LOGS_DIR, "screenlog.$screen_id"); backup_home_log($home_id, $log_file); - + my $precmd = ""; my $postcmd = ""; - + $postcmd .= generate_post_install_scripts($mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list); - + my $bash_scripts_path = MANUAL_TMP_DIR . "/home_id_" . $home_id; - + if ( check_b4_chdir($bash_scripts_path, $owner) != 0) { return -1; } - + my $installfile = create_bash_scripts($mods_full_path, $bash_scripts_path, $precmd, $postcmd, @installcmds); - + my $screen_cmd = create_screen_cmd($screen_id, "./$installfile"); - + logger "Installing Steam Workshop content on server Home ID " . $home_id; system($screen_cmd); - + return 1; } @@ -5409,10 +5582,10 @@ sub generate_post_install_scripts { my ($mods_full_path, $workshop_id, $mods_list, $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, + $variable, $place_after, $mod_string, + $string_separator, $config_file_path, $post_install, $mod_names_list) = @_; - + my $post_install_scripts = ""; my $mods_info_path = Path::Class::Dir->new(AGENT_RUN_DIR, 'WorkshopModsInfo'); $post_install_scripts .= "mods_full_path=\"$mods_full_path\"\n". @@ -5426,7 +5599,7 @@ sub generate_post_install_scripts "mods_info_path=\"$mods_info_path/\"\n"; my @workshop_mods = split /,/, $mods_list; my @mod_names = split /,/, $mod_names_list; - + my $index = 0; foreach my $workshop_mod_id (@workshop_mods) { @@ -5434,14 +5607,14 @@ sub generate_post_install_scripts my $workshop_mod_path = $mods_full_path.$steamcmd_download_path; my $this_mod_string = $mod_string; $this_mod_string =~ s/\%workshop_mod_id\%/$workshop_mod_id/g; - + $post_install_scripts .= "mod_string[$index]=\"$this_mod_string\"\n". "mod_name[$index]=\"".$mod_names[$index]."\"\n". "workshop_mod_id[$index]=\"$workshop_mod_id\"\n". "workshop_mod_path[$index]=\"$workshop_mod_path\"\n"; $index++; } - + $post_install_scripts .= 'if [ ! -e $config_file_path ];then'."\n". ' if [ ! -d "$(dirname $config_file_path)" ];then mkdir -p "$(dirname $config_file_path)";fi'."\n". ' echo -e "${place_after}\n${variable}" > $config_file_path'."\n". @@ -5450,7 +5623,7 @@ sub generate_post_install_scripts 'for mod_id in "${workshop_mod_id[@]}"'."\n". 'do'."\n". ' first_file="$(ls "${workshop_mod_path[$i]}"| sort -n | head -1)"'."\n"; - + my @post_install_lines = split /[\r\n]+/, $post_install; foreach my $line (@post_install_lines) { if($line ne ""){ @@ -5460,7 +5633,7 @@ sub generate_post_install_scripts $post_install_scripts .= "\t".$line."\n"; } } - + $post_install_scripts .= ' file_content=$(cat $config_file_path)'."\n". ' if [[ $file_content =~ $regex ]]; then'."\n". ' full_match="${BASH_REMATCH[0]}"'."\n". @@ -5470,7 +5643,7 @@ sub generate_post_install_scripts ' found=0'."\n". ' fi'."\n". ' first_file_string="\%first_file%"'."\n". - + ' if [ -z "${mod_string[$i]##*$first_file_string*}" ];then'."\n". ' mod_string[$i]="${mod_string[$i]/$first_file_string/$first_file}"'."\n". ' fi'."\n". @@ -5507,9 +5680,9 @@ sub generate_post_install_scripts sub get_workshop_mods_info() { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); - + my $mods_info_dir_path = Path::Class::Dir->new(AGENT_RUN_DIR, 'WorkshopModsInfo'); - + if(-d $mods_info_dir_path) { opendir(MODS_INFO_DIR, $mods_info_dir_path) or return -1; @@ -5535,14 +5708,14 @@ sub get_workshop_mods_info() closedir(MODS_INFO_DIR); return "1;".encode_list(@mods_info); } - + return -1; } sub get_setting_using_api { my ($setting_name) = @_; - + if(defined WEB_API_URL && WEB_API_URL ne "" && defined WEB_ADMIN_API_KEY && WEB_ADMIN_API_KEY ne ""){ my $url = WEB_API_URL . "?setting/get&setting_name=" . $setting_name . "&token=" . WEB_ADMIN_API_KEY; my $ua = LWP::UserAgent->new; @@ -5552,7 +5725,7 @@ sub get_setting_using_api my $response = $ua->get($url); return $response->decoded_content(); } - + return -1; } @@ -5576,7 +5749,7 @@ sub get_minecraft_rcon_port{ } close(FH); - + return $port; } @@ -5590,7 +5763,7 @@ sub generate_random_password{ my @alphanumeric = ('a'..'z', 'A'..'Z', 0..9,'!','_','-'); my @numeric = (0..9); my $randpassword = ''; - + if(not defined $length || not is_integer($length)){ $length = 16; } @@ -5602,8 +5775,8 @@ sub generate_random_password{ return $randpassword; } -sub trim{ - my $s = shift; - $s =~ s/^\s+|\s+$//g; - return $s -} \ No newline at end of file +sub trim{ + my $s = shift; + $s =~ s/^\s+|\s+$//g; + return $s +} diff --git a/COMPANION_PROGRAMS_DESIGN.md b/COMPANION_PROGRAMS_DESIGN.md new file mode 100644 index 00000000..317fe4b4 --- /dev/null +++ b/COMPANION_PROGRAMS_DESIGN.md @@ -0,0 +1,866 @@ +# Companion Programs Design Investigation + +This is an investigation-only design report for adding first-class companion/sidecar application support to GSP game servers. No implementation is included here. + +Repository layout reviewed: + +- `/Agent-Windows` +- `/Agent_Linux` +- `/Panel` +- `/Website` + +Note: the repository currently uses `Agent_Linux` on disk, not `Agent-Linux`. + +## 1. Current Flow Found + +### Main Agent Files + +Relevant files: + +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` +- `Panel/includes/lib_remote.php` +- `Panel/modules/gamemanager/mini_start.php` +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/start_server.php` +- `Panel/modules/gamemanager/stop_server.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/server_config_parser.php` +- `Panel/modules/config_games/config_servers.php` +- `Panel/modules/config_games/cli-params.php` + +### Agent Startup Directory And Autostart + +Both agents define an agent run directory and a startup flag directory: + +- Linux: `AGENT_RUN_DIR`, `GAME_STARTUP_DIR`, `SCREEN_LOGS_DIR`, `SCREENRC_FILE` +- Windows/Cygwin: same general constants, with Windows-specific SteamCMD executable paths + +Both agents create/read the `startups` directory on agent startup. If startup files exist and `--no-startups` is not set, the agent reads each startup file and calls `universal_start_without_decrypt(...)` with the saved startup arguments. + +Startup files are CSV-like records containing: + +```text +home_id, home_path, server_exe, run_dir, startup_cmd, server_port, server_ip, cpu, nice, preStart, envVars, game_key, console_log +``` + +These files are useful for agent autostart but should not be treated as runtime status truth. + +### Screen Usage + +Both agents use `screen` as the shared runtime backend. + +Linux: + +- `create_screen_id(SCREEN_TYPE_HOME, $home_id)` creates names like `OGP_HOME_000000123`. +- `create_screen_cmd(...)` returns a `screen -d -m -t ... -S ...` command. +- `create_screen_cmd_loop(...)` creates a generated shell script named like `OGP_HOME_000000123_startup_scr.sh`, then runs that script inside `screen`. +- The generated shell script loops/restarts the server when autorestart is enabled. +- The script checks for `SERVER_STOPPED` before respawning. + +Windows/Cygwin: + +- Uses the same `create_screen_id(...)` naming pattern. +- `create_screen_cmd(...)` launches a command in `screen`. +- `create_screen_cmd_loop(...)` writes `_serverStart.bat`, then runs it through `cmd /Q /C` inside `screen`. +- The generated batch file loops/restarts the server when autorestart is enabled. +- The batch file checks `SERVER_STOPPED` before respawning. + +### Start Flow + +Panel start path: + +- `Panel/modules/gamemanager/mini_start.php` builds `$start_cmd`. +- It collects XML `pre_start`, `environment_variables`, `lock_files`, executable name, executable location, port, IP, CPU affinity, nice value, game key, and console log. +- It calls `$remote->universal_start(...)`. + +Linux agent start: + +- `universal_start_without_decrypt(...)` validates the home path and executable. +- It may create a per-game Linux user when that preference is enabled. +- It writes/updates the startup flag file in `GAME_STARTUP_DIR`. +- It converts `pre_start` and environment variables between multiline and startup-safe formats. +- It builds the command according to executable type: + - `.exe` / `.bat`: Wine command + - `.jar`: startup command + - other: `./server_exe startup_cmd` +- It creates a generated shell startup script through `create_screen_cmd_loop(...)`. +- It backs up `screenlogs/screenlog.`. +- It runs pre-start commands through `run_before_start_commands(...)`. +- It launches the generated command through `sudo_exec_without_decrypt($cli_bin, $owner)`. +- It renices server processes after launch. + +Windows/Cygwin agent start: + +- `universal_start_without_decrypt(...)` validates the home path and executable. +- It converts paths with `cygpath -wa`. +- It converts `pre_start` and environment variables. +- It builds Windows commands using `cmd /Q /C start ... /WAIT`. +- With autorestart enabled, it uses `create_screen_cmd_loop(...)` and `_serverStart.bat`. +- Without autorestart, it uses `create_screen_cmd(...)`. +- It backs up `screenlogs/screenlog.`. +- It runs pre-start commands through `run_before_start_commands(...)`. +- It launches the screen command with `system($cli_bin)`. +- It writes a startup file named `_serverStart.bat` under `GAME_STARTUP_DIR`, though the name is generic rather than per IP/port in the current Windows path. + +### Stop Flow + +Panel stop path: + +- `Panel/modules/gamemanager/stop_server.php` calls `$remote->remote_stop_server(...)`. +- Stop parameters include home ID, IP, port, control protocol, control password, control type, and home path. + +Linux agent stop: + +- `stop_server_without_decrypt(...)` removes the startup flag for `$server_ip-$server_port`. +- If autorestart is enabled, it creates `SERVER_STOPPED` in the game home. +- It attempts a graceful stop when configured: + - `rcon` + - `rcon2` + - `armabe` + - `minecraft` +- If graceful stop does not complete, it collects PIDs from the screen session using `get_home_pids(...)`. +- It sends `kill 15`, then `kill 9` if needed. +- It runs `screen -wipe`. + +Windows/Cygwin agent stop: + +- `stop_server_without_decrypt(...)` removes the startup flag. +- If autorestart is enabled, it creates `SERVER_STOPPED` in the game home. +- It finds the screen PID from `screen -list`. +- It maps that to a Windows PID using `ps -W`. +- It calls `cmd /C taskkill /f /fi 'PID eq ...' /T`. +- It runs `screen -wipe`. + +### Restart Flow + +Both agents expose `restart_server` and `restart_server_without_decrypt(...)`. + +The current intended restart flow is: + +1. Stop server. +2. Wait 60 seconds. +3. Start server. + +That flow is now present in the current agent files. Any companion system should hook into this sequence explicitly: + +1. Stop companions. +2. Stop game server. +3. Wait 60 seconds. +4. Start game server. +5. Start companions after their configured delays. + +### PID Tracking + +Current PID tracking exists but is inconsistent: + +- The agents write their own `ogp_agent.pid`. +- FastDownload has `fd.pid`. +- Scheduler has `scheduler.pid`. +- Linux game server child PIDs are discovered from the screen PID with `pgrep -P`. +- Windows/Cygwin maps a screen PID to a Windows PID through `ps -W`. +- Some game XML uses the `PID_FILE` CLI param, for example Source-engine servers. +- Windows `_alsoRun.bat` behavior expects `_alsoRun.pid` to contain helper PIDs. + +There is no general companion PID registry today. + +### Marker Files + +Relevant marker files: + +- `GAME_STARTUP_DIR/-` startup flag on Linux. +- Windows writes a startup file under `GAME_STARTUP_DIR`, currently `_serverStart.bat`. +- `SERVER_STOPPED` in the game home when autorestart is enabled. +- `_alsoRun.pid` for the current Windows helper behavior. + +`SERVER_STOPPED` is used by generated autorestart scripts to decide whether to respawn. It should not be used as the source of truth for current runtime status. + +### Logs + +Game screen logs are written under: + +```text +screenlogs/screenlog. +``` + +The agents call `backup_home_log(...)` before starting a server. If XML defines `console_log`, Panel/agent log functions may read the game-specific console log instead. + +There is no dedicated companion stdout/stderr log path today. + +## 2. Current Helper Script Behavior + +### `_alsoRun.bat` + +The clearest current companion behavior is Windows-only. + +In `Agent-Windows/ogp_agent.pl`, `create_screen_cmd_loop(...)` generates `_serverStart.bat`. That generated batch file contains: + +```bat +if exist "_alsoRun.bat" call "_alsoRun.bat" +start ... /wait +for /f %%p in (_alsoRun.pid) do taskkill /PID %%p /F +``` + +This means: + +- `_alsoRun.bat` is executed before the game server process. +- `_alsoRun.pid` is read after the game server exits. +- Each PID listed in `_alsoRun.pid` is force-killed. +- This only exists in the Windows/Cygwin agent path. +- It only works when the generated `_serverStart.bat` loop path is used. +- It relies on files in or near the game server working directory. +- It depends on the helper script correctly writing `_alsoRun.pid`. + +Some DayZ/Arma XML files generate `_alsoRun.bat` in `pre_start`. Examples found include: + +- `Panel/modules/config_games/server_configs/dayz_arma2co_win32.xml` +- `Panel/modules/config_games/server_configs/dayz_epoch_mod_win32.xml` + +Those XML files build `_alsoRun.bat` to start BEC and use WMIC to find/write the BEC PID into `_alsoRun.pid`. + +Problems: + +- The helper file lives in a customer-accessible game area. +- The customer may be able to edit/delete `_alsoRun.bat` or `_alsoRun.pid`. +- It is Windows-specific. +- PID capture depends on executable path matching and WMIC. +- Cleanup happens after the game process exits, not as a first-class stop action. +- It is not centrally visible to the Panel as companion state. + +### `pre_start` + +The XML schema supports `pre_start`. + +Panel reads `pre_start` in: + +- `Panel/modules/gamemanager/mini_start.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/gamemanager/home_handling_functions.php` + +The value is passed to the agent as `$preStart`. + +Linux behavior: + +- `run_before_start_commands(...)` writes `${home_path}/prestart_ogp.sh`. +- It writes each XML line into the shell file. +- It runs `bash prestart_ogp.sh` as the game owner. +- The script removes itself at the end. + +Windows/Cygwin behavior: + +- `run_before_start_commands(...)` writes `_prestart.bat` in the Windows-converted home path. +- It writes each XML line into the batch file. +- It runs `cmd /Q /C start /wait "<_prestart.bat>"`. +- It deletes `_prestart.bat`. + +Concerns: + +- `pre_start` is trusted XML/admin-defined script content, but it executes in/against the customer game home. +- It is not structured. +- It is not tracked. +- It is only "before start"; it is not a companion lifecycle model. +- It cannot reliably stop helper processes unless custom script authors implement that themselves. + +### `post_start` + +The schema and config editor order include `post_start`. + +Found references: + +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/config_servers.php` +- `Panel/modules/config_games/xml_tag_descriptions.php` +- One example XML: `Panel/modules/config_games/server_configs/nexuiz_win.xml` + +I did not find a start path that reads `server_xml->post_start` or sends it to the agents. As currently written, `post_start` appears schema-visible but not operational. + +### `post_install` + +Both agents include workshop/mod post-install script generation logic. This is install/update-related, not game-server runtime lifecycle management. + +`post_install` should not be used for companion processes because it is not tied to server start/stop/restart. + +### Custom Fields And Custom Scripts + +Custom fields are supported in XML and stored per server. They are used by config replacement logic: + +- `Panel/modules/gamemanager/cfg_text_replace.php` +- `Panel/modules/config_games/schema_server_config.xml` + +Custom fields can influence config file content. They are not currently a safe or structured way to define executable companion commands. + +Customers can also use extra startup parameters where permissions allow. That should not become the companion command source because it would permit arbitrary command injection into managed helper launches. + +## 3. Recommended Method 1 + +### First-Class Companion Programs System + +The best design is a first-class, structured companion program system. + +Game XML should define the allowed companion programs for that game. The Panel should store per-server choices such as enabled/disabled and optional safe settings. The agent should own runtime launch, PID tracking, log paths, stop, and restart cleanup. + +Example XML shape: + +```xml + + + 0 + 30 + {GAME_PATH}\BEC + Bec.exe + -f Config.cfg + Bec.exe + 1 + 1 + companions/bec.out.log + companions/bec.err.log + + +``` + +Recommended XML fields: + +- `key`: stable internal ID. +- `name`: display name. +- `os`: `linux`, `windows`, or `any`. +- `enabled_default`: default panel setting. +- `delay_seconds_default`: default delay after game start. +- `working_dir`: trusted template path. +- `command`: trusted executable/script name or path template. +- `args`: trusted default arguments. +- `process_name`: optional verification aid only, not the kill target by itself. +- `stop_command`: optional graceful stop command. +- `stop_timeout_seconds`: default graceful wait. +- `kill_after_timeout`: boolean. +- `stop_on_server_stop`: boolean. +- `restart_on_server_restart`: boolean. +- `stdout_log` / `stderr_log`: relative managed log paths. +- `required_files`: optional list for Panel validation. +- `requires_game_online`: whether to delay until the game port is online. + +### Where XML Defines Allowed Companion Programs + +Add `companion_programs` to `Panel/modules/config_games/schema_server_config.xml` as an optional top-level element. + +Only admins/editors of game XML should be able to define: + +- executable path +- arguments +- working directory +- stop command +- process name +- default delay +- log paths + +Customers should not be able to enter arbitrary commands. + +### Where Panel Stores Per-Server Settings + +Panel should store per-home companion settings in the database, not in customer-editable files. + +Possible table: + +```sql +server_companion_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + home_id INT NOT NULL, + companion_key VARCHAR(64) NOT NULL, + enabled TINYINT(1) NOT NULL DEFAULT 0, + delay_seconds INT NULL, + settings_json LONGTEXT NULL, + UNIQUE KEY uniq_home_companion (home_id, companion_key) +) +``` + +`settings_json` should only contain safe, schema-defined options. Do not allow free-form command or shell text. For example: + +- config profile selection +- config file name from an allowed list +- delay override within min/max bounds +- environment preset key + +### How The Agent Receives Settings + +Extend the encrypted XML-RPC start/restart call to include a companion payload, or add a dedicated agent RPC after start: + +Recommended: + +- `universal_start(..., companion_payload)` for direct lifecycle coupling. +- `server_companion_start(home_id, payload)` for explicit start action. +- `server_companion_stop(home_id)` for explicit stop action. +- `server_companion_status(home_id)` for status display. + +The payload should be generated by the Panel from trusted XML plus stored per-server enabled settings. + +Payload should contain fully resolved, validated data: + +```json +{ + "home_id": 123, + "server_ip": "1.2.3.4", + "server_port": 2302, + "companions": [ + { + "key": "bec", + "enabled": true, + "delay_seconds": 30, + "working_dir": "/home/ogp_agent/OGP_User_Files/123/BEC", + "command": "Bec.exe", + "args": ["-f", "Config.cfg"], + "stdout_log": "bec.out.log", + "stderr_log": "bec.err.log" + } + ] +} +``` + +For XML-RPC compatibility, this may be passed as JSON text. + +### How The Agent Starts Companions + +The agent should start companions after the game screen/session has launched. + +Recommended approach: + +- Use separate `screen` sessions per companion. +- Session naming: + - Game: `OGP_HOME_000000123` + - Companion: `OGP_COMPANION_000000123_bec` +- Write companion runtime state under an agent-controlled path outside the game FTP/file-manager root. +- Start delayed companions through a small agent-owned delay wrapper: + - Linux: `screen -d -m -S OGP_COMPANION_... bash -lc 'sleep 30; cd ...; exec ...'` + - Windows/Cygwin: `screen -d -m -S OGP_COMPANION_... cmd /Q /C "timeout /t 30 ... && cd /d ... && ..."` + +This avoids blocking game startup. The Panel can show the game as `STARTING`/`ONLINE` independently while companion statuses move through `PENDING`, `STARTING`, `RUNNING`, or `FAILED`. + +### How The Agent Records PIDs + +Create an agent-owned runtime directory, for example: + +```text +/companions// +``` + +Files: + +```text +companions//state.json +companions//pids/.pid +companions//logs/.out.log +companions//logs/.err.log +``` + +State should include: + +- home ID +- companion key +- screen session name +- agent PID/screen PID +- child PID if known +- command hash or executable path +- start time +- last status +- last error + +For Linux: + +- Prefer launching through `screen`. +- Use the screen PID plus child process lookup. +- If possible, make the wrapper write `echo $$` and the exec child PID to a PID file. +- Use process group/session cleanup where possible. + +For Windows/Cygwin: + +- Keep `screen` for parity. +- Prefer a wrapper that starts the companion and writes the Windows PID to a PID file. +- Use Cygwin `ps -W` or PowerShell/WMIC alternatives where available. +- Avoid killing by executable name alone. + +### How The Agent Stops Companions + +Stop by the recorded handle, not by executable name. + +Recommended stop order: + +1. Load `companions//state.json`. +2. For each running companion: + - If `stop_command` exists, run it in the configured working directory. + - Wait up to `stop_timeout_seconds`. + - If still alive and `kill_after_timeout=1`, kill only recorded PID/process tree/session. + - Close the companion screen session. + - Update state. +3. Clean stale PID files only after verifying the process is gone. + +Avoid: + +- `pkill Bec.exe` +- `taskkill /IM Bec.exe` +- broad executable-name matching + +Those can kill unrelated customer processes. + +### Restart Behavior + +Recommended restart sequence: + +1. Stop companions for the home. +2. Stop game server. +3. Wait 60 seconds. +4. Start game server. +5. Start enabled companions according to delay settings. + +Do not let companions survive a restart unless explicitly marked as persistent and safe. + +### Companion Logging + +Companion stdout/stderr should be logged separately from game screen logs. + +Recommended default: + +```text +/companions//logs/.out.log +/companions//logs/.err.log +``` + +Panel can add a companion log viewer later, separate from the game server log viewer. + +For customer visibility, expose logs through the agent RPC after permission checks rather than placing them inside the customer FTP root by default. + +### Security + +This method is secure because: + +- Commands come from trusted game XML/admin configuration. +- Customer UI stores only enable/disable and bounded safe settings. +- Runtime files live outside the customer file manager/FTP root. +- The agent launches only known companion keys for the current game config. +- Stop/kill uses recorded PIDs/screen sessions, not executable names. +- Logs are controlled by the agent. + +### Portability + +This method is portable because: + +- XML can define Linux and Windows variants with the same logical companion key. +- Agent receives normalized companion payload. +- Both agents use screen sessions. +- PID capture is platform-specific behind the same agent RPC model. + +## 4. Recommended Method 2 + +### Agent-Managed Generated Startup/Cleanup Scripts + +Alternate design: keep using scripts, but generate and store them in an agent-controlled directory outside customer-editable game files. + +Example layout: + +```text +/companions// + companions.json + start_companions.sh + stop_companions.sh + start_companions.bat + stop_companions.bat + pids/ + logs/ +``` + +The Panel still defines companions from trusted XML and stores per-server enabled settings. The agent generates scripts from those trusted definitions. + +Start flow: + +1. Game server launches. +2. Agent starts an agent-owned companion startup script in its own screen session. +3. Script handles delays and PID capture. + +Stop flow: + +1. Agent runs an agent-owned cleanup script. +2. Script kills only PIDs listed in the agent-owned `pids` directory. +3. Agent verifies cleanup. + +### Pros + +- Closer to the existing `_serverStart.bat` and generated shell script model. +- Easier to debug for admins because generated scripts are readable. +- Can work with the existing screen backend. +- Avoids customer-editable `_alsoRun.bat`. +- Can be implemented incrementally. + +### Cons + +- Still script-heavy. +- Quoting rules remain difficult across Linux, Cygwin, and Windows. +- Risk of script injection if generation is not strict. +- PID capture still needs platform-specific care. +- State management can drift if scripts are edited manually by admins. +- Less clean than direct agent-managed process APIs. + +### Comparison To Method 1 + +Method 1 is better long term because the agent owns lifecycle state directly. Method 2 is a reasonable migration bridge if implementation speed matters, but it should still use trusted XML/admin definitions and agent-owned control paths. + +## 5. Security Considerations + +### Do Not Trust Customer-Editable Startup Files + +Avoid using customer-editable files such as: + +- `_alsoRun.bat` +- `_alsoRun.pid` +- arbitrary `.bat` or `.sh` helper files in the game home + +Those files can be edited, deleted, replaced, or used to run unintended commands. + +### Managed Files Should Live Outside The FTP/File Manager Root + +Recommended location: + +```text +/companions// +``` + +This directory should be owned by the agent or game server runner user and should not be directly writable by customers. + +### Commands Must Come From Trusted Configuration + +Companion commands should come from: + +- shipped game XML +- admin-managed XML +- a future admin-only companion catalog + +They should not come from: + +- customer text fields +- startup extra parameters +- uploaded files +- customer-editable config files + +### Validate Commands + +Validation should include: + +- companion key exists in the current game XML +- OS matches the agent OS +- command path resolves under an allowed directory unless explicitly admin-approved +- working directory resolves under game home or an admin-approved path +- no unexpanded `{...}` tokens remain +- no shell metacharacters in fields that are supposed to be argv tokens +- delay is numeric and capped +- log paths are relative and cannot escape managed log directories + +### Avoid Arbitrary Command Execution + +Use argv-style execution where possible. + +When shell/batch is unavoidable: + +- generate commands from trusted fields only +- quote every path/argument consistently +- avoid concatenating customer-provided strings into shell code +- keep generated files outside customer write paths + +### Avoid Killing Unrelated Processes + +Do not kill by executable name alone. + +Preferred kill targets: + +- recorded companion PID +- recorded process group +- recorded screen session +- recorded Windows process tree for that PID + +Process name should be used only as a verification hint, not as the primary kill selector. + +## 6. Cross-platform Considerations + +### Linux Agent + +Linux can use: + +- `screen` +- `bash` +- PID files +- `/proc` +- `pgrep -P` +- `kill 15`, then `kill 9` + +Recommended Linux companion launch: + +```text +screen -d -m -S OGP_COMPANION__ bash -lc '' +``` + +Use an agent-owned wrapper or direct fork/exec to record PID and redirect logs. + +### Windows/Cygwin Agent + +Windows/Cygwin can use: + +- `screen` +- `cmd /Q /C` +- `start` +- `taskkill /PID ... /T` +- `ps -W` +- possibly WMIC or PowerShell depending on environment + +Current `_alsoRun.bat` depends on WMIC in some XML. WMIC is not reliable on all modern Windows installations, so future code should not require it. + +Recommended Windows companion launch: + +```text +screen -d -m -S OGP_COMPANION__ cmd /Q /C "" +``` + +The wrapper should record the Windows PID or enough process/session information for cleanup. + +### Batch vs Shell Differences + +Risks: + +- quoting spaces in paths +- escaping backslashes +- environment variable syntax differences +- `start` window title behavior on Windows +- delayed expansion in batch files +- Cygwin path vs Windows path conversion +- signal semantics: Linux signals vs Windows taskkill + +The Panel should not build platform command strings. It should send trusted structured definitions to the agent and let the agent perform OS-specific quoting/execution. + +### Screen Behavior + +Screen remains the default shared backend for now. + +Use separate screen sessions for companions rather than attaching them to the game server screen. This gives: + +- independent status +- independent logs +- safer cleanup +- clearer restart behavior + +### Process Cleanup Risks + +Companion processes may: + +- fork children +- daemonize +- spawn background processes +- exit while leaving children +- fail before writing PID +- be started manually by the customer/admin + +The agent should track process trees where possible and verify stop results. + +## 7. Suggested Implementation Phases + +### Phase 1: Inventory Current Flow And Add Report Only + +This report is Phase 1. + +No source code changes should be made for companion behavior in this phase. + +### Phase 2: XML Schema Design + +Add `companion_programs` to: + +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/config_servers.php` +- `Panel/modules/config_games/xml_tag_descriptions.php` + +Create example XML for BEC, B3, and a generic log watcher. + +### Phase 3: Panel Storage/UI Design + +Add storage for per-server companion settings. + +Initial UI: + +- show companion list from XML +- enable/disable checkbox +- delay override +- safe predefined options only + +Do not expose raw command editing to customers. + +### Phase 4: Agent Start/Stop Integration + +Add encrypted RPCs: + +- `server_companion_start` +- `server_companion_stop` +- `server_companion_status` + +Extend start/restart flow to call companion start/stop at the right time. + +### Phase 5: PID Tracking And Cleanup + +Implement: + +- agent-owned state directory +- PID files +- screen session names +- process tree cleanup +- stale PID detection +- companion status reporting + +### Phase 6: DayZ/BEC Test + +Migrate one DayZ/Arma BEC case away from `_alsoRun.bat`. + +Test: + +- start game +- delayed BEC start +- BEC status +- stop game +- BEC cleanup +- restart flow +- crash/recovery behavior +- agent restart behavior + +### Phase 7: Generalized Docs + +Document: + +- XML authoring +- admin setup +- supported placeholders +- security rules +- troubleshooting +- log locations +- migration path from `_alsoRun.bat` + +## 8. Open Questions + +1. Should companions be started after the game process exists, after the game port is listening, or after query/RCON succeeds when available? +2. Should companions be allowed to keep running if the game server crashes and autorestarts, or should they always restart with each game process cycle? +3. Should some companions be marked "persistent" across game restarts? +4. Which user should run companions on Linux when `LINUX_USER_PER_GAME_SERVER` is enabled? +5. Should companion logs be visible to customers by default, or admin-only? +6. Should companion config files be editable through the existing config file module? +7. How should Windows PID capture work on systems without WMIC? +8. Is PowerShell guaranteed available in the supported Windows/Cygwin agent environment? +9. Should the Panel support companion install/update packages, or only runtime start/stop of already-installed files? +10. Should companion definitions live only in game XML, or should there be a reusable global companion catalog? +11. How should firewall rules handle companion ports if a companion needs one? +12. What should happen if a companion fails but the game server is online? +13. Should companion failure affect billing/status/customer-facing uptime? +14. How should agent autostart restore companion state after power loss or agent restart? +15. Should the agent stop companions before or after graceful game stop for tools that send shutdown messages to the game? +16. Should companion environment variables be separate from game environment variables? +17. How should command placeholders be standardized across Linux and Windows? +18. What is the maximum acceptable startup delay for companion programs? +19. Should admins be able to override companion commands per server, or only per game XML? +20. How should existing `_alsoRun.bat` game configs be migrated without breaking current customers? + +## Summary Recommendation + +Build Method 1: a first-class companion programs system driven by trusted XML/admin configuration, stored per server in the Panel, and executed/owned by the agent. + +Do not continue expanding `_alsoRun.bat`. It is a useful proof of need, but it is Windows-specific, customer-editable, hard to audit, and unreliable for stop/restart cleanup. + +Use `screen` as the shared backend for now, with one screen session per companion and agent-owned PID/log/state files outside the customer file root. diff --git a/GSP_PLATFORM_IMPROVEMENT_REPORT.md b/GSP_PLATFORM_IMPROVEMENT_REPORT.md new file mode 100644 index 00000000..3ef6c733 --- /dev/null +++ b/GSP_PLATFORM_IMPROVEMENT_REPORT.md @@ -0,0 +1,1020 @@ +# GSP Platform Improvement Report + +Investigation date: 2026-06-05 + +Repository reviewed: + +- `Agent-Windows` +- `Agent_Linux` +- `Panel` +- `Website` + +Scope: investigation and roadmap only. No source code changes are included in this report. + +## 1. Executive Summary + +GSP already has the pieces of a serious game server hosting platform: a panel, Linux and Windows/Cygwin agents, XML-based game definitions, file management, FTP, server lifecycle controls, RCON, scheduler hooks, add-on/content modules, support/ticket modules, billing/provisioning work, and utilities for popular server ecosystems. + +The current risk is not lack of features. The risk is uneven reliability and inconsistent integration. Some modules are mature enough to keep, some are partially modernized, and some are clearly legacy/testing code that should be hidden until replaced. A professional customer-facing platform should first make the core server experience accurate and trustworthy: + +- Start, stop, restart, and status must reflect actual agent truth. +- Logs must update live and help users diagnose startup failures. +- File, FTP, and config editing must feel like the customer owns the server while staying inside safe boundaries. +- Backups must be real, visible, restorable, and scheduled. +- Scheduler actions must be safe predefined operations, not arbitrary commands for customers. +- Add-ons, Steam Workshop, mods, and companion programs should be first-class managed systems rather than customer-edited helper scripts. +- Documentation should be tied directly to the selected game, common config files, ports, startup parameters, mods, and troubleshooting. + +The platform philosophy should be: + +> GSP should not try to hide game server administration behind shallow buttons. It should give customers secure control over their server files, startup parameters, logs, mods, schedules, backups, and documentation, while keeping host files, agent controls, credentials, and unsafe commands protected. + +The roadmap should prioritize the foundation first: + +1. Reliable lifecycle and status reporting. +2. Live logs and startup diagnostics. +3. Safe file/FTP/config editing. +4. Working backups and restore. +5. Scheduler cleanup with typed actions and logs. +6. Server content and Steam Workshop as managed jobs. +7. Support/docs/billing polish. +8. Optional advanced modules such as companion programs, utility tools, notifications, and game-specific integrations. + +## 2. Current System Findings + +### Repository Layout + +| Path | Purpose | Notes | +|---|---|---| +| `Agent-Windows` | Windows/Cygwin game server agent | Uses Perl, screen, Windows/Cygwin-compatible commands, Steam/update helpers, log access, start/stop/restart operations. | +| `Agent_Linux` | Linux game server agent | Uses Perl, screen, sudo execution, Steam/update helpers, log access, start/stop/restart operations. | +| `Panel` | Main web control panel | PHP modules, XML game configs, database logic, agent XML-RPC communication, UI. | +| `Website` | Public website / customer-facing site | Separate from Panel; should link cleanly into billing/support/dashboard flows. | + +Note: the user-facing prompt refers to `/Agent-Linux`, but the actual local directory is `Agent_Linux`. + +### Panel-Agent Communication + +The Panel communicates with agents through `Panel/includes/lib_remote.php`, which wraps XML-RPC calls to agent methods. Important calls include: + +- `universal_start` +- `remote_stop_server` +- `remote_restart_server` +- `remote_server_status` +- `is_screen_running` +- `get_log` +- `remote_query` +- file read/write/list operations +- scheduler operations +- update/install operations + +The agents expose corresponding Perl subroutines through `ogp_agent.pl`. Both agents use `screen` as the shared backend for game server sessions and update/install jobs. + +### Lifecycle Status Direction + +The dirty worktree already contains lifecycle/status changes that appear to move toward an agent-owned structured status model: + +- `server_status_without_decrypt` exists in both agents. +- `Panel/includes/lib_remote.php` contains `remote_server_status`. +- `Panel/modules/gamemanager/home_handling_functions.php` contains helper logic for agent server status. +- `Panel/modules/gamemanager/server_monitor.php` consumes agent status. + +The intended direction is correct: marker files and LGSL/GameQ query success should not be the source of truth. The agent should report process/session and port readiness, while query data is optional metadata. + +### Screen Backend + +Both agents use screen sessions named in the `OGP__` pattern. This makes Linux and Windows/Cygwin behavior similar, which is useful for portability. + +Current screen-related behavior includes: + +- Home server screen sessions. +- Update/install screen sessions. +- Screen log files under agent run screen log paths. +- Start wrapper scripts generated around commands. +- Stop logic that attempts graceful stop, screen quit, and process cleanup. + +Screen should remain the shared backend for now, but GSP should improve the status model around it. + +### Marker Files + +The agents still contain `SERVER_STOPPED` marker-file references. These marker files can be useful as loop/autorestart hints, but they must not be trusted as the actual server state. Crash, failed start, power loss, and agent restart can leave marker files wrong. + +### Query Systems + +The Panel and agents still include GameQ/LGSL-style query support. This should remain useful for: + +- hostname +- map +- player count +- server rules +- extra metadata + +It should not determine whether a server is online. A server can be healthy while query is unsupported, slow, blocked, or temporarily unavailable. + +### Current Design Tension + +The current system mixes: + +- OGP legacy modules. +- GSP custom business/billing modules. +- Newer partial systems such as Server Content Manager. +- Older modules that are present but not production-ready. + +The improvement plan should avoid one giant rewrite. The better path is progressive consolidation around a few strong interfaces: + +- Agent status API. +- Agent job API. +- Safe file roots. +- XML capability declarations. +- Database-backed task/content/backup state. +- Panel pages that show state, logs, and next actions clearly. + +## 3. Current Module Inventory + +| Module | Current State | Keep/Remove/Improve | Why | Priority | +|---|---|---|---|---| +| `gamemanager` | Core server monitor, start, stop, restart, logs, RCON presets, updates. | Improve | This is the center of the customer experience. Lifecycle accuracy and logs must be excellent. | Critical | +| `config_games` | XML game definitions, CLI parameter tooling, mod config mapping. | Improve | This should become the source of game capabilities, startup templates, docs hooks, workshop support, scheduler support. | Critical | +| `user_games` | Server assignment, creation, install commands, expiration, custom fields. | Improve | Provisioning correctness and billing tie-ins depend on this. | Critical | +| `server` | Remote server/node management. | Keep/Improve | Needed for agent/node administration and capacity management. | Critical | +| `litefm` | In-panel file manager/editor, appears to include Ace editor assets. | Improve | Should become the preferred safe in-browser file editor. | High | +| `ftp` | FTP/net2ftp-style module and FTP permissions. | Improve/Audit | FTP is important for advanced users, but embedded legacy tools need security review. | High | +| `editconfigfiles` | Game-specific editable config file shortcuts. | Improve | Useful for customers who want common files surfaced without browsing folders. | High | +| `addonsmanager` | Renamed/commented as Server Content Manager with newer tables for Workshop/content manifests/history. | Improve | Best current home for mods, add-ons, scripts, config packs, and Workshop. | High | +| `steam_workshop` | Marked deprecated; navigation removed; DB helpers retained for compatibility. | Hide/Migrate | Should not compete with Server Content Manager. Preserve data migration path only. | Medium | +| `cron` | Scheduler/CRON module, stores jobs on agents, supports user/admin actions. | Improve | Needs safe typed actions, DB logs, conflict handling, and better UX. | High | +| `backup-restore` | Explicitly says not working/testing; hard-coded paths and credentials style. | Hide/Replace | Unsafe and not production-ready. Replace with managed backup system. | Critical | +| `tickets` | Richer support ticket module with tables, messages, attachments, settings. | Keep/Improve | Better foundation than simple support page. Add server context/log attachments. | High | +| `support` | Basic support entry module. | Keep as landing | Should route to tickets, docs, and server-specific support requests. | Medium | +| `news` | Legacy optional NewsLister-style module. | Hide or Modernize | Useful only if converted into polished announcements. | Low | +| `circular` | Notification/circular module. | Improve | Better candidate for admin notices and maintenance announcements than old news. | Medium | +| `billing` | Large custom billing/provisioning/customer shop area with docs and payment gateways. | Improve | Important for commercial hosting, but should be tested as part of provisioning lifecycle. | High | +| `mysql` | MySQL admin/database module. | Keep/Future | Useful future customer database hosting tie-in. | Medium | +| `fast_download` | FastDL support and access rules. | Keep | Useful for Source/GoldSrc communities. | Medium | +| `rcon` | Admin RCON module. | Keep/Integrate | RCON should support scheduler warnings, console commands, and diagnostics. | High | +| `tshock` | Terraria/TShock-specific controls. | Keep Conditional | Expose only for Terraria/TShock servers and document setup. | Medium | +| `util` | AMX, SourceMod, Steam ID, agent/network tools. | Improve/Hide legacy | Useful tools should remain, outdated helpers should be hidden or labeled legacy. | Medium | +| `faq` | FAQ module. | Improve | Could become part of a game documentation/help system. | Medium | +| `update` | Panel update module with patch/backup concepts. | Keep Admin | Useful for admin maintenance, not customer server management. | Medium | +| `status` | Alpha/admin status module. | Hide/Improve | Could become node health dashboard later. | Low | +| `subusers` | Subuser management. | Keep/Improve | Important commercial feature for teams and communities. | High | +| `dashboard` | Panel landing/dashboard. | Improve | Should surface server health, billing status, support, docs, and alerts. | High | +| `TS3Admin`, `teamspeak3` | Voice server related modules. | Keep if sold | Hide if not part of current product line. | Low | +| `dsi`, `lgsl_with_img_mod`, `mods` | Legacy/niche modules. | Audit | Keep only if actively used. | Low | + +## 4. Broken / Risky / Outdated Features + +| Feature/Module | Risk | Recommendation | +|---|---|---| +| `backup-restore` | Contains explicit "not working/testing" messaging, hard-coded paths, hard-coded panel URL, hard-coded SSH port, and shell command construction. | Hide from customer UI immediately. Replace with agent-managed backup jobs. | +| Marker-file state checks | `SERVER_RUNNING` / `SERVER_STOPPED` can become stale. | Treat marker files only as loop hints. Agent process/session/port status is source of truth. | +| LGSL/GameQ as online proof | Some games do not query reliably or start slowly. | Query must be optional metadata only. | +| Agent restart shortcuts | Restart logic is historically fragile when separate from stop/start truth. | Restart should mean stop, wait, start, with status polling and logs. | +| Scheduler raw commands | Admin scheduler can run raw commands; API token is embedded in generated wget command; output is discarded. | Customer scheduler must use predefined safe actions. Raw command stays admin-only with audit logs. | +| `--no-check-certificate` in scheduler command | Weakens transport trust. | Use verified HTTPS by default. Only allow override with admin warning. | +| Scheduler state on agent flat file only | Panel lacks durable task run history and structured result state. | Store schedules and task runs in Panel DB; agent executes jobs and reports status. | +| Legacy FTP/net2ftp tooling | Old embedded third-party file tooling may include outdated libraries. | Security audit or migrate to LiteFM as primary UI while retaining FTP service support. | +| Duplicate support systems | `support` and `tickets` both exist, with different depth. | Make `support` a landing page and `tickets` the actual workflow. | +| Deprecated `steam_workshop` module | Separate from newer Server Content Manager. | Hide and migrate data into Server Content Manager. | +| Server Content Manager partial Workshop support | Current structure is promising but lacks professional mod state, load order, async jobs, and per-game install strategies. | Continue as the home for Workshop/content, but build a complete job/state model. | +| Post scripts/custom scripts | Can become arbitrary command execution if exposed too freely. | Restrict customer input; commands should come from trusted XML/admin definitions. | +| Scattered docs | Billing docs, module docs, XML docs, FAQ, and external docs are not unified in server pages. | Build a game-aware documentation surface linked from monitor/config/content pages. | +| Large legacy utility set | AMX/SourceMod/Steam ID tools may be useful but can look outdated. | Hide until verified, then expose per-game where relevant. | + +## 5. Modules to Keep + +These modules are core or strategically useful: + +- `gamemanager` +- `config_games` +- `user_games` +- `server` +- `litefm` +- `ftp` +- `editconfigfiles` +- `addonsmanager` +- `cron` +- `tickets` +- `support` +- `billing` +- `subusers` +- `rcon` +- `fast_download` +- `mysql` +- `faq` +- `update` + +Keep does not mean "leave as-is." Most of these need cleanup and integration, but they belong in the future product. + +## 6. Modules to Remove or Hide + +Remove should usually mean "hide from customer navigation until replaced," not immediate deletion. + +| Module | Recommendation | Reason | +|---|---|---| +| `backup-restore` | Hide immediately | Explicitly not working and uses hard-coded backup infrastructure. | +| `steam_workshop` | Hide from UI, keep migration helpers | Deprecated by Server Content Manager. | +| `news` | Hide unless modernized | Old NewsLister-style approach is not a polished support/notification system. | +| `status` | Hide or admin-only | Alpha module should not be customer-facing. | +| `dsi` | Audit/hide if unused | Unknown current business purpose. | +| `mods` | Audit/hide if replaced by Server Content Manager | Avoid duplicate mod/content concepts. | +| `lgsl_with_img_mod` | Admin/legacy only | Query images are secondary; do not confuse status reporting. | +| `TS3Admin`, `teamspeak3` | Hide unless voice hosting is sold | Only expose if part of product offering. | + +## 7. Modules to Improve + +| Module | Improvement Needed | +|---|---| +| `gamemanager` | Agent-truth status, clear state badges, startup diagnostics, current log excerpts, restart as stop-wait-start, force stop UX, query metadata note. | +| `config_games` | XML capability schema for startup timeout, query availability, RCON, scheduler actions, Workshop, companion programs, docs, editable config files. | +| `user_games` | Provisioning validation, safer defaults, billing lifecycle clarity, post-create checks, install logs, port collision handling. | +| `litefm` | Safe root enforcement, editor backups, syntax highlighting by extension, read-only protected dirs, large file handling. | +| `ftp` | Security audit, FTPS/SFTP guidance, credential rotation, clear user docs, avoid exposing control paths. | +| `editconfigfiles` | Better game-specific config shortcuts, docs next to fields, backup-before-save. | +| `addonsmanager` | Full Server Content system: install jobs, progress logs, Workshop metadata, load order, enable/disable, update/uninstall. | +| `cron` | Typed action registry, task logs, status, no customer raw shell, overlap prevention, missed-task handling. | +| `tickets` | Server context, recent logs, screenshots/attachments, priority, staff assignment, notifications. | +| `billing` | Provisioning audit, server expiration behavior, invoices, service suspension/recovery, payment event logs. | +| `rcon` | Safe presets, scheduler integration, warning messages, output capture. | +| `util` | Modernize useful tools, hide outdated helpers, scope by game. | +| `tshock` | Capability-gated Terraria/TShock UI, setup docs, credential handling. | + +## 8. Commercial Hosting Feature Comparison + +Public references reviewed: + +- Nitrado automated tasks: https://server.nitrado.net/en-GB/guides/automated-tasks-en/ +- Nitrado panel navigation/backups: https://server.nitrado.net/en-GB/guides/navigating-nitrado-server-panel +- Nitrado backup management: https://server.nitrado.net/en-US/guides/how-to-manage-and-restore-nitrado-server-backups/ +- Nodecraft file access: https://nodecraft.com/features/full-file-access +- Nodecraft control panel knowledgebase: https://nodecraft.com/support/knowledgebase/nodepanel +- TCAdmin features: https://www.tcadmin.com/features/ +- TCAdmin game tools docs: https://docs.tcadmin.com/using-tcadmin/game-tools/ +- CubeCoders AMP product pages: https://cubecoders.com/AMP/7d2d and https://www.cubecoders.com/AMP/TQ +- Shockbyte scheduled tasks: https://shockbyte.com/help/knowledgebase/articles/creating-scheduled-tasks-with-shockbyte-panel +- Shockbyte backups: https://shockbyte.com/help/knowledgebase/articles/how-to-backup-your-server-files +- Host Havoc scheduled backups: https://hosthavoc.com/billing/knowledgebase/479/Creating-Scheduled-Backups.html +- Host Havoc backup restore: https://hosthavoc.com/billing/knowledgebase/428/CreatingorRestoring-backups.html +- PingPerfect gamepanel overview: https://test.pingperfecthost.us/index.php/knowledgebase/405/Gamepanel-Explained.html +- PingPerfect scheduled server messages: https://pingperfect.com/knowledgebase/706/7-Days-to-Die--Scheduled-Server-Messages.html + +| Feature | GSP Current | Commercial Pattern | Recommended GSP Improvement | +|---|---|---|---| +| Start/stop/restart | Exists, but historically mixed marker/query/screen assumptions. | One-click power controls with status feedback. | Agent-truth lifecycle with OFFLINE, STARTING, ONLINE, STOPPING, UNRESPONSIVE, UNKNOWN. | +| File manager | LiteFM and FTP/net2ftp exist. | Built-in file manager, editor, uploads, archives, search, FTP/SFTP. | Make LiteFM primary and FTP secondary; add safe roots, backups, syntax highlighting. | +| FTP access | Existing FTP module and credentials. | Full file access with FTP/SFTP/FTPS. | Keep FTP, document it clearly, support credential rotation, avoid control-file exposure. | +| Config editing | `editconfigfiles` and custom fields exist. | Common config files surfaced in friendly editors. | Build per-game config shortcuts from XML docs and editable file declarations. | +| Startup parameters | Last params/custom fields exist. | Simple settings plus advanced startup args. | Use template + advanced mode, validate paths/flags, show docs. | +| Live console/logs | Existing log viewer; recent work improved AJAX refresh. | Real-time console/log view with commands where supported. | Keep AJAX-only log updates, add startup log, dated log detection, highlight common errors. | +| Backups | Current backup module is not production-ready. | Manual and scheduled backups, restore/download, retention. | Replace with agent-managed backup jobs and Panel DB history. | +| Scheduler | Exists, but command-oriented and agent-file driven. | Scheduled restarts, backups, commands, updates, warnings. | Typed action registry, task run logs, notifications, conflict handling. | +| Mods/addons | Server Content Manager is partially built. | One-click mods/plugins/Workshop where game supports it. | First-class Server Content jobs with per-game strategies and DayZ/Arma/Rust paths. | +| Workshop | Deprecated module plus partial Server Content scripts. | TCAdmin advertises Workshop browser and one-click installs. | URL/ID first version, metadata later, load order, startup integration, update/uninstall. | +| RCON/console commands | RCON module and presets exist. | Console/RCON commands and scheduled warnings. | Capability-gate by game, validate presets, capture command output. | +| Support | Basic support plus richer tickets module. | Tickets/live chat/docs links. | Use tickets as primary, attach server context and recent logs. | +| Docs | Scattered docs and FAQ. | Knowledgebase per game and task. | Game-aware docs panel generated from XML and curated links. | +| Subusers | Exists. | Role/permission sharing. | Improve permissions around files, power actions, billing, content, schedules. | +| Notifications | Circular/news/support pieces exist. | Panel/email/Discord notices. | Unified notifications for tasks, outages, restarts, billing, support. | + +What GSP can do better than many hosts: + +- Keep advanced file/startup control rather than hiding everything. +- Make status honest even when game query is unsupported. +- Show exactly why a server is STARTING, ONLINE, STOPPING, or UNRESPONSIVE. +- Tie docs directly to the user's selected game, config file, and current action. +- Provide safe structured automation without blocking advanced users from manual config. +- Keep agent behavior portable across Linux and Windows/Cygwin. + +## 9. Recommended GSP Feature Set + +### Customer-Facing Core + +- Server dashboard with clear status and current action. +- Start, stop, restart, force stop. +- Live logs and downloadable logs. +- File manager and FTP credentials. +- Common config file shortcuts. +- Startup parameter editor with simple and advanced modes. +- Server Content / Mods / Workshop page. +- Backups page with create, restore, download, and retention. +- Scheduler page with safe predefined actions. +- Support ticket button with server context. +- Game documentation page. +- Subuser permissions. + +### Advanced Customer Features + +- RCON/console command presets. +- Scheduled warnings before restart. +- Scheduled Workshop/mod updates. +- Clone mod list or config set to another server. +- Repair/validate server files. +- Restore edited config files. +- Optional companion programs if allowed by game/admin config. + +### Admin Features + +- Node health and capacity dashboard. +- Game XML capability editor. +- Server provisioning audit. +- Port allocation and collision report. +- Backup storage policy. +- Content install strategy definitions. +- Scheduler action registry. +- Support ticket triage. +- Billing/service lifecycle reporting. +- Secure role and permission management. + +## 10. Server Lifecycle Improvement Plan + +### Target Status Model + +| Status | Meaning | +|---|---| +| `OFFLINE` | No managed process/session and no required listening game port. | +| `STARTING` | Managed session/process exists but required game port is not listening yet. | +| `ONLINE` | Managed session/process exists and required game port is listening. | +| `STOPPING` | Stop was requested and the managed session/process still exists. | +| `UNRESPONSIVE` | Session/process exists but port did not become ready after timeout, or stop failed. | +| `UNKNOWN` | Panel cannot reach the agent or agent cannot determine state. | + +### Source of Truth + +The agent must be the source of truth. Readiness should be based on: + +1. Managed screen/session existence. +2. Process/PID when known. +3. Required game port listening. +4. Optional query metadata only after readiness. + +Do not mark a server failed only because LGSL/GameQ does not answer. + +### Start + +Recommended behavior: + +1. Panel sends start request. +2. Agent launches managed screen/session. +3. If session launches, Panel displays STARTING immediately. +4. Panel polls agent status. +5. If process/session exists and game port listens, Panel displays ONLINE. +6. If startup timeout expires without port readiness, display UNRESPONSIVE with log excerpt and troubleshooting links. + +Per-game XML should declare: + +- default startup timeout +- game port variable +- query port variable +- whether query is expected/reliable +- startup log path patterns +- common startup errors + +Slow-start games like Arma/DayZ should use longer defaults such as 180 seconds or more. + +### Stop + +Recommended behavior: + +1. Panel sends stop request. +2. Agent sends graceful stop command if configured. +3. Agent marks STOPPING hint. +4. Agent polls process/session and port. +5. If still running after timeout, escalate to screen quit / process kill. +6. Report stopped only when session/process is gone and port is not listening. +7. If still running, return UNRESPONSIVE and show kill/force-stop option if permitted. + +### Restart + +Restart should be simple: + +1. Stop server. +2. Wait 60 seconds. +3. Start server. + +Do not use a separate shortcut that bypasses stop/start status checks. Display restart as a sequence: stopping, waiting, starting, online/unresponsive. + +### Force Stop + +Force stop should be available with confirmation. It should: + +- kill only the managed session/process tree for that server. +- avoid executable-name-based killing. +- log the action. +- record user/admin who clicked it. + +## 11. File/FTP/Config Editing Plan + +### Philosophy + +Customers should feel like they own the server. They should be able to edit config files, upload mods, download worlds, use FTP, and inspect logs. GSP's job is to keep them inside the game server boundary and protect agent/control files. + +### Safe File Boundaries + +Customer file access should be limited to: + +- game install/home path +- declared save/config/log paths +- optional allowed mod/content paths + +Customer file access should not include: + +- agent run directory +- screen logs outside exposed copies +- Panel secrets +- control manifests that can cause command execution +- global cache directories unless read-only and explicitly allowed +- other users' homes + +### File Manager + +Improve LiteFM as the primary file manager: + +- browse, upload, download, rename, move, copy, delete. +- zip/unzip where safe. +- text editor with syntax highlighting. +- large file warnings. +- read-only handling for protected files. +- backup-before-save option. +- restore last edited version. +- path validation on every request. + +### FTP + +Keep FTP for advanced users: + +- show host, port, username, and reset password. +- support TLS/FTPS/SFTP where available. +- clear docs for FileZilla/WinSCP. +- rotate credentials. +- disable FTP per server if needed. +- never expose agent control directories. + +### Config Shortcuts + +Use `editconfigfiles` and XML declarations to surface common files: + +- `serverDZ.cfg` +- `server.cfg` +- `GameUserSettings.ini` +- `Engine.ini` +- `server.properties` +- `oxide/config/*.json` +- `tshock/config.json` + +Each shortcut should show: + +- what the file controls. +- whether restart is required. +- link to official docs. +- backup/restore button. + +## 12. Documentation System Plan + +### Current State + +Docs exist in scattered places: + +- `Panel/docs` +- `Panel/documentation` +- `Panel/modules/billing/docs` +- FAQ module +- module-specific docs +- XML guide/editor pages + +The docs are valuable but not integrated strongly enough into the server workflow. + +### Recommended Docs Model + +Create a game-aware documentation surface: + +- Server Overview: quick start, status meaning, where logs are. +- Files: common config files and save paths. +- Startup Parameters: supported flags and examples. +- Ports: game/query/RCON/Steam ports. +- Mods/Workshop: game-specific install behavior. +- Scheduler: safe recommended tasks. +- Backups: what is included/excluded. +- Troubleshooting: common startup and query failures. + +### XML Integration + +Game XML files should be extended to declare docs: + +```xml + + docs/dayz/getting-started.md + + +``` + +Docs should open in the Panel and external links should open in a new tab. + +### Trusted Internet Resources + +Use official docs first: + +- game publisher/developer docs. +- Steam dedicated server docs. +- Bohemia Interactive docs for DayZ/Arma. +- Facepunch docs for Rust. +- Valve docs for Source/SteamCMD. + +Third-party guides should be labeled as community resources. + +## 13. Scheduler/Automation Plan + +### Current Scheduler Findings + +The current `cron` module: + +- provides admin and user scheduler pages. +- can create cron-like jobs on agents. +- builds commands that call Panel API endpoints through `wget`. +- stores tasks primarily on the agent side. +- supports start, stop, restart, Steam update, and newer Server Content actions. +- still allows raw command scheduling in admin context. + +Limitations: + +- task output is commonly discarded. +- Panel lacks durable structured task run history. +- API token is embedded in generated command text. +- no clear overlap/conflict control. +- no missed-task handling after downtime. +- no polished customer task log. +- no typed action argument schema. + +### Recommended Action Groups + +Customer-safe: + +- restart server. +- stop server. +- start server. +- create backup. +- update installed mods. +- send restart warning. +- run allowed RCON preset. +- rotate logs. +- check server status. + +Advanced customer: + +- validate/repair server files. +- update SteamCMD game files. +- scheduled wipe/reset if game XML supports it. +- restore latest backup. +- clone backup/config to another server. + +Admin-only: + +- arbitrary shell command. +- raw script execution. +- permission repair. +- force kill. +- node cleanup. +- storage cleanup. +- global cache cleanup. + +### Proposed Action Registry + +Each action should have: + +- action key. +- display name. +- description. +- allowed roles. +- required permissions. +- supported OS. +- required game XML capability. +- arguments schema. +- validation rules. +- timeout. +- retry behavior. +- conflict rules. +- log policy. + +Example: + +```yaml +action_key: restart_server +display_name: Restart Server +role: customer +agent_action: stop_wait_start +args: + warning_minutes: integer + warning_message: string +timeout_seconds: 600 +conflicts_with: + - update_server + - restore_backup + - install_content +``` + +### Task Logs + +Each scheduled run should have: + +- pending/running/success/failed/skipped/canceled. +- start and end time. +- server ID. +- node/agent. +- action key. +- user/admin that created the task. +- output log. +- error summary. +- next run time. + +## 14. Addons/Workshop/Mods Plan + +### Current State + +`addonsmanager` has already been renamed conceptually to Server Content Manager. Its schema includes: + +- `addons` +- `server_content_workshop` +- `server_content_manifest` +- `server_content_install_history` +- install method fields +- workshop item/app fields +- target path templates +- launch parameter additions +- policy fields for required/blocked Workshop IDs + +This is the correct module to improve. + +The old `steam_workshop` module is deprecated and should stay hidden except for migration/backward compatibility. + +### Target System + +Server Content should support: + +- scripts. +- mods. +- plugins. +- config packs. +- Steam Workshop items. +- maps. +- server extensions. +- companion support packages if needed. + +### Workshop First Version + +First version should allow: + +1. user enters Workshop URL or numeric ID. +2. Panel extracts numeric ID. +3. Panel validates against game XML Workshop capability. +4. user adds item to queue. +5. agent runs SteamCMD download. +6. agent installs content using trusted strategy. +7. Panel tracks install state and logs. +8. user can enable/disable/reorder. +9. startup parameters update from structured mod list. + +### DayZ / Arma Requirements + +DayZ and Arma-style games need: + +- download Workshop item. +- normalize install folder name. +- copy/install to `@ModName` style folder. +- copy `.bikey` files into `keys`. +- update startup line, for example `-mod=@CF;@ModTwo`. +- preserve load order. +- allow disable without deleting. +- allow update/uninstall. +- prompt restart when server is running. + +### XML Capability Example + +```xml + + 1 + steam + 221100 + 221100 + steamcmd + dayz_mod_folder + {GAME_PATH} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {GAME_PATH}/keys + + +``` + +### Job Model + +Workshop/content installs should be asynchronous jobs: + +- queued. +- running. +- success. +- failed. +- canceled. + +The Panel should poll progress logs instead of blocking a page load. + +## 15. Support/Newsletter/Utilities/TShock Plan + +### Support + +Use `support` as a simple customer entry page, but make `tickets` the real support workflow. + +Server-specific support should include: + +- server ID/home ID. +- game name/mod. +- node/agent. +- current agent status. +- recent lifecycle events. +- recent log excerpt. +- last failed scheduler/content/backup job. +- customer message. +- optional screenshots/attachments. + +### Tickets + +The tickets module already includes: + +- ticket table. +- message table. +- attachment table. +- settings table. +- attachment limits. +- notification setting. + +Improvements: + +- attach server context automatically. +- add staff assignment and status filters. +- add customer-visible timeline. +- allow staff to request logs/backups. +- support ticket links from each server page. + +### Newsletter / News / Circular + +The old `news` module should not be the primary notification system unless modernized. + +Use or improve `circular` for: + +- maintenance notices. +- node outages. +- product announcements. +- billing reminders. +- planned restarts. + +Notifications should appear in: + +- dashboard. +- server monitor. +- email when configured. +- Discord webhook later. + +### Utilities + +Keep useful utilities but clean presentation: + +- Steam ID converter. +- network/agent tools for admins. +- SourceMod/AMX helpers only for relevant games. + +Hide or label legacy utilities that are not verified. + +### TShock + +The TShock module can be valuable for Terraria hosting, but it should be exposed only when: + +- game XML indicates Terraria/TShock support. +- required ports/tokens are configured. +- documentation exists. + +TShock features should include: + +- users. +- bans. +- token/status. +- docs for enabling REST/RCON safely. + +## 16. Security Plan + +### File Boundaries + +All file operations must validate: + +- resolved real path. +- allowed root. +- no path traversal. +- no symlink escape unless explicitly permitted. +- file size limits for editor. +- extension/content rules for executable uploads if needed. + +### Command Execution + +Customers should not be allowed to schedule or run arbitrary shell commands by default. + +Allowed customer operations should map to trusted actions: + +- start. +- stop. +- restart. +- backup. +- update. +- install content. +- run approved RCON preset. + +Admin-only raw commands must be: + +- permission-gated. +- logged. +- time-limited. +- clearly marked dangerous. + +### Startup Parameters + +Startup parameters should allow advanced control without arbitrary host command execution: + +- structured variable substitution. +- whitelist/validation where practical. +- path normalization. +- documented advanced mode. +- no injection into shell wrappers without escaping. + +### Secrets + +Protect: + +- Steam credentials. +- RCON passwords. +- FTP passwords. +- API tokens. +- billing gateway keys. +- SMTP credentials. +- agent encryption keys. + +Logs should redact secrets. Scheduler command files should not expose API tokens where avoidable. + +### Roles and Permissions + +Permissions should cover: + +- power controls. +- force stop. +- file manager. +- FTP. +- config edit. +- startup params. +- mods/content. +- backups. +- scheduler. +- RCON. +- billing. +- support. +- subuser management. + +### Audit Logs + +Record: + +- who started/stopped/restarted. +- who edited files. +- who changed startup params. +- who installed/uninstalled mods. +- who restored backups. +- who ran RCON commands. +- who created scheduler tasks. +- admin raw commands. + +## 17. User Experience Plan + +### Server Overview + +The server monitor should answer these questions immediately: + +- Is my server offline, starting, online, stopping, unresponsive, or unknown? +- What is GSP checking to decide that? +- Are the process/session and port present? +- Is query info available? +- What is the latest log output? +- What can I do next? + +### Clear Status Language + +Examples: + +- ONLINE: "Server process is running and the game port is listening." +- STARTING: "Server process started. Waiting for the game port." +- ONLINE, query unavailable: "Server is online. Player/map query is unavailable for this game or did not respond." +- UNRESPONSIVE: "Server process is running, but the game port did not open before the timeout." +- UNKNOWN: "The agent could not be reached." + +### Common Actions + +Each server page should expose: + +- Power. +- Logs. +- Files. +- Config. +- Startup. +- Mods/Content. +- Backups. +- Scheduler. +- Support. +- Docs. + +### Mobile + +Mobile pages should prioritize: + +- current status. +- start/stop/restart. +- latest log. +- support. +- docs. + +Large file management can remain desktop-focused, but it should not break on mobile. + +## 18. Admin Experience Plan + +Admins need tools for: + +- node health and agent connectivity. +- server state audit. +- stuck server detection. +- port allocation. +- game XML capability management. +- provisioning tests. +- billing lifecycle. +- backup storage usage. +- content job failures. +- scheduler task failures. +- support ticket triage. + +Admin pages should distinguish: + +- customer-visible issue. +- node/agent issue. +- provisioning issue. +- billing issue. +- game XML/config issue. + +## 19. Implementation Phases + +| Phase | Priority | Files/Modules Involved | Notes | +|---|---|---|---| +| Phase 1: Lifecycle validation | Critical | `Agent-Windows/ogp_agent.pl`, `Agent_Linux/ogp_agent.pl`, `Panel/includes/lib_remote.php`, `Panel/modules/gamemanager` | Finish and test agent-truth status, start, stop, restart, log viewer. | +| Phase 2: Hide unsafe legacy modules | Critical | `backup-restore`, `steam_workshop`, `news`, `status`, nav/module settings | Hide customer access to broken/deprecated modules before launch. | +| Phase 3: Live logs and diagnostics | Critical | `gamemanager/log.php`, `view_server_log.php`, agent `get_log` | Dated log discovery, startup logs, download logs, error highlighting. | +| Phase 4: File/FTP safety audit | High | `litefm`, `ftp`, `editconfigfiles`, agent file methods | Safe roots, editor backups, protected dirs, FTP docs, credential reset. | +| Phase 5: Backup system replacement | High | new backup tables/jobs, agents, scheduler | Replace `backup-restore` with real create/restore/download/retention. | +| Phase 6: Scheduler action registry | High | `cron`, agents, Panel DB | Typed actions, task logs, conflict rules, safe customer actions. | +| Phase 7: Server Content foundation | High | `addonsmanager`, agents, XML schema | Async jobs, progress logs, install history, enable/disable. | +| Phase 8: Steam Workshop / DayZ | High | `addonsmanager`, SteamCMD scripts, `config_games` | Workshop URL/ID, DayZ key copy, load order, startup param integration. | +| Phase 9: Docs integration | High | `faq`, `config_games`, `billing/docs`, server pages | Game-specific docs linked from server workflows. | +| Phase 10: Support polish | Medium | `support`, `tickets`, `circular` | Server-context tickets, notifications, maintenance notices. | +| Phase 11: Billing/provisioning audit | High | `billing`, `user_games`, APIs | Validate server creation, expiration, suspension, passwords, FTP. | +| Phase 12: Companion programs | Medium | agents, XML schema, Panel UI | PID-tracked BEC/B3/bot/log watcher support. | +| Phase 13: Utilities/TShock | Medium | `util`, `tshock`, XML capabilities | Expose only where relevant and document setup. | +| Phase 14: Commercial polish | Medium | Dashboard, UX, notifications | Smooth customer experience, mobile, alerts, user education. | +| Future: infrastructure services | Future | `mysql`, new service modules | DB hosting, Git/Forgejo, web hosting, LXC/container services. | + +## 20. Open Questions + +1. Which games are launch priorities for first commercial release? +2. Should Windows/Cygwin remain a first-class target long term, or only compatibility? +3. What FTP server is expected in production: FTP, FTPS, SFTP, or multiple? +4. Where should customer backups be stored: same node, separate backup node, object storage, or multiple options? +5. What is the desired retention policy by plan? +6. Should customers be allowed to edit all files under game home, or should some files be protected by default? +7. How should advanced startup params be limited to prevent shell injection while preserving real control? +8. Which games require authenticated SteamCMD credentials? +9. How should Steam credentials be stored and rotated? +10. Should Workshop cache be global per node, per user, or per server only? +11. Should GSP support SFTP directly or rely on existing FTP tooling first? +12. Which billing gateway is production priority: PayPal, Stripe, manual, or all? +13. How should expired/suspended servers behave for files, backups, FTP, scheduler, and support? +14. Which notification channels are required first: panel, email, Discord? +15. Should customers be able to grant subusers billing access, or only technical access? +16. Should raw scheduled commands ever be customer-accessible under an advanced plan, or always admin-only? +17. Is containerization a near-term requirement or a future product line? + +## 21. Final Recommendations + +1. Treat lifecycle/status/logging as the launch gate. GSP cannot be professional if it lies about server state. +2. Keep the agent as source of truth. Use screen/session, PID, and port checks; keep query data optional. +3. Hide `backup-restore` until replaced. A broken backup module is worse than no backup module. +4. Use LiteFM plus FTP as the customer file-control foundation, but audit path boundaries and legacy FTP tooling. +5. Consolidate mods/addons/Workshop under Server Content Manager. Keep `steam_workshop` hidden for migration only. +6. Replace scheduler command strings with a typed action registry and DB-backed task run logs. +7. Make docs part of the server workflow, not a separate afterthought. +8. Use tickets as the real support workflow and include server context automatically. +9. Keep advanced users empowered: files, FTP, startup params, logs, RCON, mods, and schedules should remain accessible. +10. Keep dangerous operations structured and permissioned: raw shell commands, host paths, credentials, and agent files must stay protected. +11. Build improvements in phases. Do not rewrite the entire panel at once. + +The most important commercial differentiator for GSP should be honest control: customers can manage servers like they own them, while GSP provides secure boundaries, accurate state, automation, backups, docs, and support. diff --git a/Panel/includes/lib_remote.php b/Panel/includes/lib_remote.php index d433f125..77f32468 100644 --- a/Panel/includes/lib_remote.php +++ b/Panel/includes/lib_remote.php @@ -627,8 +627,8 @@ class OGPRemoteLibrary /// \return 1 If is /// \return 0 If is not /// \return -1 If agent could not be reached. - public function is_screen_running($screen_type,$home_id) - { + public function is_screen_running($screen_type,$home_id) + { $params = $this->encrypt_params($screen_type,$home_id); $this->add_enc_chk($params); $request = xmlrpc_encode_request("is_screen_running", $params); @@ -638,10 +638,40 @@ class OGPRemoteLibrary else if ( $status === 0 ) return 0; else - return -1; - } + return -1; + } - public function mon_stats() + public function remote_server_status($home_id, $server_ip, $server_port, $query_port = "", $rcon_port = "", $startup_timeout = 180, $state_hint = "") + { + $params = $this->encrypt_params($home_id, $server_ip, $server_port, $query_port, $rcon_port, $startup_timeout, $state_hint); + $this->add_enc_chk($params); + $request = xmlrpc_encode_request("server_status", $params); + $status = $this->sendRequest($request); + + if (is_array($status) && !xmlrpc_is_fault($status)) { + return $status; + } + + return array( + 'status' => 'UNKNOWN', + 'ready' => 0, + 'process_running' => 0, + 'session_running' => 0, + 'game_port_listening' => 0, + 'query_port_listening' => 0, + 'rcon_port_listening' => 0, + 'pid' => '', + 'session_name' => '', + 'ip' => $server_ip, + 'port' => $server_port, + 'query_port' => $query_port, + 'rcon_port' => $rcon_port, + 'last_error' => 'Agent status RPC unavailable.', + 'query_info' => '' + ); + } + + public function mon_stats() { $args = $this->encrypt_params("mon_stats"); $this->add_enc_chk($args); diff --git a/Panel/modules/gamemanager/home_handling_functions.php b/Panel/modules/gamemanager/home_handling_functions.php index 8196001c..12575aaf 100644 --- a/Panel/modules/gamemanager/home_handling_functions.php +++ b/Panel/modules/gamemanager/home_handling_functions.php @@ -36,6 +36,45 @@ function get_query_port($server_xml, $server_port) { return $server_port; } +function get_agent_server_status($remote, $server_xml, $home_id, $server_ip, $server_port, $state_hint = "", $startup_timeout = 180) +{ + $query_port = ""; + $rcon_port = ""; + + if (isset($server_xml->protocol) && (string)$server_xml->protocol === "gameq") { + $query_port = get_query_port($server_xml, $server_port); + } elseif (isset($server_xml->protocol) && (string)$server_xml->protocol === "lgsl" && isset($server_xml->lgsl_query_name)) { + require_once('protocol/lgsl/lgsl_protocol.php'); + $get_ports = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $server_port, "", ""); + $query_port = isset($get_ports['1']) ? $get_ports['1'] : ""; + } elseif (isset($server_xml->protocol) && (string)$server_xml->protocol === "teamspeak3") { + $query_port = "10011"; + } + + if (isset($server_xml->rcon_port) && is_numeric((string)$server_xml->rcon_port)) { + $rcon_port = (string)$server_xml->rcon_port; + } + + if (!method_exists($remote, 'remote_server_status')) { + return array('status' => 'UNKNOWN', 'last_error' => 'Panel remote library does not support agent status.'); + } + + return $remote->remote_server_status($home_id, $server_ip, $server_port, $query_port, $rcon_port, $startup_timeout, $state_hint); +} + +function is_agent_status_online($agent_status) +{ + return is_array($agent_status) && isset($agent_status['status']) && strtoupper($agent_status['status']) === 'ONLINE'; +} + +function is_agent_status_active($agent_status) +{ + if (!is_array($agent_status) || !isset($agent_status['status'])) { + return false; + } + return in_array(strtoupper($agent_status['status']), array('STARTING', 'ONLINE', 'STOPPING', 'UNRESPONSIVE')); +} + function get_start_cmd($remote,$server_xml,$home_info,$mod_id,$ip,$port,$db) { $last_param = json_decode($home_info['last_param'], True); @@ -250,19 +289,23 @@ function exec_operation( $action, $home_id, $mod_id, $ip, $port, $override = fal } } - $screen_running = $remote->is_screen_running(OGP_SCREEN_TYPE_HOME,$home_info['home_id']) === 1; + $agent_status = get_agent_server_status($remote, $server_xml, $home_info['home_id'], $ip, $port); + $screen_running = is_agent_status_active($agent_status); if ( $action == "stop" AND $screen_running ) { - $remote_retval = $remote->remote_stop_server($home_info['home_id'], - $ip, $port, $server_xml->control_protocol, - $home_info['control_password'],$server_xml->control_protocol_type, $home_info['home_path']); - $db->logger(get_lang_f('server_stopped', $home_info['home_name'] ) . "($ip:$port)"); - if ( $remote_retval === -1 ) - return FALSE; - elseif( $remote_retval === -2 ) - return FALSE; - else + $remote_retval = $remote->remote_stop_server($home_info['home_id'], + $ip, $port, $server_xml->control_protocol, + $home_info['control_password'],$server_xml->control_protocol_type, $home_info['home_path']); + $agent_status = get_agent_server_status($remote, $server_xml, $home_info['home_id'], $ip, $port, "STOPPING"); + $db->logger(get_lang_f('server_stopped', $home_info['home_name'] ) . "($ip:$port)"); + if ( $remote_retval === -1 ) + return FALSE; + elseif( $remote_retval === -2 ) + return FALSE; + elseif( isset($agent_status['status']) && strtoupper($agent_status['status']) !== "OFFLINE" ) + return FALSE; + else { $firewall_settings = $db->getFirewallSettings($home_info['remote_server_id']); if ($firewall_settings['status'] == "enable") diff --git a/Panel/modules/gamemanager/log.php b/Panel/modules/gamemanager/log.php index b41b0d04..9f0964c7 100644 --- a/Panel/modules/gamemanager/log.php +++ b/Panel/modules/gamemanager/log.php @@ -8,25 +8,25 @@ require_once("modules/config_games/server_config_parser.php"); echo "home id = " .$home_id; $user_id = $_SESSION['user_id']; - - + + $isAdmin = $db->isAdmin( $user_id ); - if($isAdmin) + if($isAdmin) $home_info = $db->getGameHome($home_id); else $home_info = $db->getUserGameHome($user_id,$home_id); - - $current_mod_info = $home_info['mods'][$mod_id]; + + $current_mod_info = $home_info['mods'][$mod_id]; $home_cfg_id = $current_mod_info['home_cfg_id']; $mod_cfg_id = $current_mod_info['mod_cfg_id']; - + if($home_cfg_id === null && $mod_cfg_id === null){ $home_cfg_id = 67; $mod_cfg_id = 68; //print_failure(get_lang('invalid_game_mod_id')); //return; } - + if ( $home_info === FALSE ) { //print_failure( get_lang("no_access_to_home") ); @@ -45,7 +45,7 @@ require_once("modules/config_games/server_config_parser.php"); $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); $home_log = ""; - + if( isset( $server_xml->console_log ) ) { $log_retval = $remote->get_log(OGP_SCREEN_TYPE_HOME, @@ -72,72 +72,50 @@ require_once("modules/config_games/server_config_parser.php"); if(hasValue($home_log)){ $home_log = utf8_encode($home_log); } - - // Using the refreshed class - if( isset($_GET['refreshed']) ) - { - echo "
".htmlentities($home_log)."
"; - } - else - { - echo "

".htmlentities($home_info['home_name'])."

"; - if($log_retval == 1) + + // Using the refreshed class + if( isset($_GET['refreshed']) ) { - require_once("includes/refreshed.php"); - - $control = '
- - - '; - if(isset($_GET['setInterval'])) - $control .= ""; - if(isset($_GET['view_player_commands'])) - $control .= ""; - $control .= ' "4000", - "8s" => "8000", - "30s" => "30000", - "2m" => "120000", - "5m" => "300000" ); - - $intSel = ' - - - '; - if(isset($_GET['size'])) - $intSel .= ""; - if(isset($_GET['view_player_commands'])) - $intSel .= ""; - $intSel .= get_lang("refresh_interval") . ':
"; - - $setInterval = isset($_GET['setInterval']) ? $_GET['setInterval'] : 4000; - $refresh = new refreshed(); - $pos = $refresh->add("home.php?m=gamemanager&p=log&type=cleared&refreshed&home_id-mod_id-ip-port=". $_GET['home_id-mod_id-ip-port']); - echo $refresh->getdiv($pos,"height:".$height.";overflow:auto;max-width:1600px;"); - ?>$intSel$control"; - if( ($server_xml->control_protocol and preg_match("/^r?l?con2?$/", $server_xml->control_protocol)) OR - ($server_xml->gameq_query_name and $server_xml->gameq_query_name == "minecraft") OR + $log_url = "home.php?m=gamemanager&p=log&type=cleared&refreshed&home_id-mod_id-ip-port=".rawurlencode($_GET['home_id-mod_id-ip-port']); + echo ''; + ?> + + control_protocol and preg_match("/^r?l?con2?$/", $server_xml->control_protocol)) OR + ($server_xml->gameq_query_name and $server_xml->gameq_query_name == "minecraft") OR ($server_xml->lgsl_query_name and $server_xml->lgsl_query_name == "7dtd") ) require('modules/gamemanager/rcon.php'); } diff --git a/Panel/modules/gamemanager/restart_server.php b/Panel/modules/gamemanager/restart_server.php index 923314f6..b6a6c4d3 100644 --- a/Panel/modules/gamemanager/restart_server.php +++ b/Panel/modules/gamemanager/restart_server.php @@ -114,77 +114,33 @@ function exec_ogp_module() { print_failure(get_lang_f('unable_to_get_log',$log_retval)); } - // If game is not supported by lgsl we skip the lgsl checks and - // assume successfull start. - if ( $home_info['use_nat'] == 1 ) - $query_ip = $home_info['agent_ip']; - else - $query_ip = $ip; - - $running = TRUE; + $startup_timeout = isset($server_xml->startup_timeout) && is_numeric((string)$server_xml->startup_timeout) ? (int)$server_xml->startup_timeout : 180; + $agent_status = get_agent_server_status($remote, $server_xml, $home_id, $ip, $port, "STARTING", $startup_timeout); + $status_name = isset($agent_status['status']) ? strtoupper($agent_status['status']) : "UNKNOWN"; - if ( $server_xml->lgsl_query_name ) - { - require('protocol/lgsl/lgsl_protocol.php'); - $get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $port, "", ""); - - //Connection port - $c_port = $get_q_and_s['0']; - //query port - $q_port = $get_q_and_s['1']; - //software port - $s_port = $get_q_and_s['2']; - - $data = lgsl_query_live((string)$server_xml->lgsl_query_name, $query_ip, $c_port, $q_port, $s_port, "sa"); - - if ( $data['b']['status'] == "0" ) - { - $running = FALSE; - } - } - elseif ( $server_xml->gameq_query_name ) - { - require_once 'protocol/GameQ/Autoloader.php'; - - $query_port = get_query_port($server_xml, $port); - - $servers = array( - array( - 'id' => 'server', - 'type' => (string)$server_xml->gameq_query_name, - 'host' => $query_ip . ":" . $query_port, - ) - ); - $gq = new \GameQ\GameQ(); - $gq->addServers($servers); - $gq->setOption('timeout', 4); - $gq->setOption('debug', FALSE); - $gq->addFilter('normalise'); - $game = $gq->process(); - - if ( ! $game['server']['gq_online'] ) - { - $running = FALSE; - } - } - - if( ! $running ) + if( $status_name !== "ONLINE" ) { if (!isset($_GET['retry'])) $retry = 0; else $retry = $_GET['retry']; - if ($retry >= 5) + if ($status_name === "UNKNOWN") { - echo "

".get_lang('server_running_not_responding')." - ".get_lang('already_running_stop_server').".

"; - echo "
<< ".get_lang('back')."
"; + print_failure(get_lang("agent_offline")); + $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port",3); + return; + } + + if ($status_name === "UNRESPONSIVE") + { + $message = isset($agent_status['last_error']) && $agent_status['last_error'] !== "" ? $agent_status['last_error'] : "Server process exists but the game port is not ready."; + print_failure(htmlentities($message)); + $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port",5); + return; } - echo "Retry #".$retry."."; + echo "

Server status: ".htmlentities($status_name).". Waiting for the game port to become ready.

"; $retry++; print("

".get_lang('starting_server')."

"); $view->refresh("?m=gamemanager&p=start&refresh&ip=$ip&port=$port&home_id=$home_id&mod_id=$mod_id&retry=".$retry,3); @@ -266,12 +222,12 @@ function exec_ogp_module() { print_failure(get_lang('server_cant_stop')); $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=". $home_id . "-". $mod_id . "-" . $ip . "-" . $port,3); } - else - { - $screen_running = $remote->is_screen_running(OGP_SCREEN_TYPE_HOME,$home_id); - if ( $screen_running == 1 ) + else { - print("

".get_lang('restarting_server')."

"); + $agent_status = get_agent_server_status($remote, $server_xml, $home_id, $ip, $port, "STARTING"); + if ( is_agent_status_active($agent_status) ) + { + print("

".get_lang('restarting_server')."

"); $view->refresh("?m=gamemanager&p=restart&refresh&ip=$ip&port=$port&home_id=$home_id&mod_id=$mod_id",3); return; } diff --git a/Panel/modules/gamemanager/server_monitor.php b/Panel/modules/gamemanager/server_monitor.php index fe9e74d8..c34d2e88 100644 --- a/Panel/modules/gamemanager/server_monitor.php +++ b/Panel/modules/gamemanager/server_monitor.php @@ -480,13 +480,21 @@ echo "is_screen_running(OGP_SCREEN_TYPE_HOME,$server_home['home_id']) === 1; + $agent_status = get_agent_server_status($remote, $server_xml, $server_home['home_id'], $server_home['ip'], $server_home['port']); + $agent_state = isset($agent_status['status']) ? strtoupper($agent_status['status']) : "UNKNOWN"; + $screen_running = is_agent_status_active($agent_status); $update_in_progress = $remote->is_screen_running(OGP_SCREEN_TYPE_UPDATE,$server_home['home_id']) === 1; if($screen_running) { // Check if the screen running the server is running. $status = "online"; $order = 1 + $j; + if($agent_state !== "ONLINE") + { + $address = "".htmlentities($agent_state).""; + if(isset($agent_status['last_error']) && $agent_status['last_error'] !== "") + $address .= " ".htmlentities($agent_status['last_error']).""; + } if ($server_xml->protocol == "lgsl") { $get_q_and_s = lgsl_port_conversion($query_name, $server_home['port'], "", ""); @@ -496,14 +504,17 @@ echo "
Connect"; + if($agent_state === "ONLINE") + $address = "Connect"; } - if ($server_xml->protocol == "teamspeak3") + if ($server_xml->protocol == "teamspeak3" && $agent_state === "ONLINE") $address = "".$displayIP.":".$server_home['port'].""; - if($server_xml->protocol == "gameq" and $server_xml->installer == 'steamcmd') + if($server_xml->protocol == "gameq" and $server_xml->installer == 'steamcmd' && $agent_state === "ONLINE") $address = "" . $displayIP . ":" . $server_home['port'] . ""; + if($agent_state === "ONLINE" && isset($agent_status['query_port']) && $agent_status['query_port'] !== "" && empty($agent_status['query_port_listening'])) + $address .= " Query info unavailable."; $pos = $refresh->add("home.php?m=gamemanager&p=ref_servermonitor&type=cleared&home_id=". $server_home['home_id'] . "&mod_id=". $server_home['mod_id'] . "&ip=" . $server_home['ip'] . "&port=" . $server_home['port']); if ($server_xml->protocol == "teamspeak3") diff --git a/Panel/modules/gamemanager/start_server.php b/Panel/modules/gamemanager/start_server.php index 9d02deb5..182d7cdd 100644 --- a/Panel/modules/gamemanager/start_server.php +++ b/Panel/modules/gamemanager/start_server.php @@ -175,79 +175,33 @@ function exec_ogp_module() startup_timeout) && is_numeric((string)$server_xml->startup_timeout) ? (int)$server_xml->startup_timeout : 180; + $agent_status = get_agent_server_status($remote, $server_xml, $home_id, $ip, $port, "STARTING", $startup_timeout); + $status_name = isset($agent_status['status']) ? strtoupper($agent_status['status']) : "UNKNOWN"; - $running = $remote->is_screen_running(OGP_SCREEN_TYPE_HOME,$home_info['home_id']); - - if ( $server_xml->lgsl_query_name ) - { - require('protocol/lgsl/lgsl_protocol.php'); - $get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $port, "", ""); - - //Connection port - $c_port = $get_q_and_s['0']; - //query port - $q_port = $get_q_and_s['1']; - //software port - $s_port = $get_q_and_s['2']; - - $data = lgsl_query_live((string)$server_xml->lgsl_query_name, $query_ip, $c_port, $q_port, $s_port, "sa"); - - if ( $data['b']['status'] == "0" ) - { - $running = FALSE; - } - } - elseif ( $server_xml->gameq_query_name ) - { - require_once 'protocol/GameQ/Autoloader.php'; - - $query_port = get_query_port($server_xml, $port); - - $servers = array( - array( - 'id' => 'server', - 'type' => (string)$server_xml->gameq_query_name, - 'host' => $query_ip . ":" . $query_port, - ) - ); - $gq = new \GameQ\GameQ(); - $gq->addServers($servers); - $gq->setOption('timeout', 4); - $gq->setOption('debug', FALSE); - $gq->addFilter('normalise'); - $game = $gq->process(); - - if ( ! $game['server']['gq_online'] ) - { - $running = FALSE; - } - } - - if( ! $running ) + if( $status_name !== "ONLINE" ) { if (!isset($_GET['retry'])) $retry = 0; else $retry = $_GET['retry']; - if ($retry > 3) + if ($status_name === "UNKNOWN") { - //echo "

".get_lang('server_running_not_responding'). - // "". - // get_lang('already_running_stop_server').".

". - //"
<< ". - $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port"); - //get_lang('back')."
"; - return; + print_failure(get_lang("agent_offline")); + $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port",3); + return; + } + + if ($status_name === "UNRESPONSIVE") + { + $message = isset($agent_status['last_error']) && $agent_status['last_error'] !== "" ? $agent_status['last_error'] : "Server process exists but the game port is not ready."; + print_failure(htmlentities($message)); + $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port",5); + return; } - //echo "Retry #".$retry."."; + echo "

Server status: ".htmlentities($status_name).". Waiting for the game port to become ready.

"; $retry++; print("

".get_lang('starting_server')."

"); $view->refresh("?m=gamemanager&p=start&refresh&ip=$ip&port=$port&home_id=$home_id&mod_id=$mod_id&retry=".$retry,3); diff --git a/Panel/modules/gamemanager/stop_server.php b/Panel/modules/gamemanager/stop_server.php index 3dc51162..540df966 100644 --- a/Panel/modules/gamemanager/stop_server.php +++ b/Panel/modules/gamemanager/stop_server.php @@ -99,10 +99,14 @@ function exec_ogp_module() { $query_port = "10011"; } - $remote_retval = $remote->remote_stop_server($home_id, - $ip, $port, $server_xml->control_protocol, - $home_info['control_password'],$control_type, $home_path); - $db->logger(get_lang_f('server_stopped', $home_info['home_name'] ) . "($ip:$port)"); + $remote_retval = $remote->remote_stop_server($home_id, + $ip, $port, $server_xml->control_protocol, + $home_info['control_password'],$control_type, $home_path); + $agent_status = get_agent_server_status($remote, $server_xml, $home_id, $ip, $port, "STOPPING"); + if ($remote_retval === 1 && isset($agent_status['status']) && strtoupper($agent_status['status']) !== "OFFLINE") { + $remote_retval = -2; + } + $db->logger(get_lang_f('server_stopped', $home_info['home_name'] ) . "($ip:$port)"); $firewall_settings = $db->getFirewallSettings($home_info['remote_server_id']); if ( $remote_retval === 1 ) { @@ -137,9 +141,13 @@ function exec_ogp_module() { } elseif ( $remote_retval === 0 ) { - $remote_retval = $remote->remote_stop_server($home_info['home_id'], - $ip, $port, $server_xml->control_protocol,"",$control_type,$home_path); - if ($remote_retval === 1 ) + $remote_retval = $remote->remote_stop_server($home_info['home_id'], + $ip, $port, $server_xml->control_protocol,"",$control_type,$home_path); + $agent_status = get_agent_server_status($remote, $server_xml, $home_id, $ip, $port, "STOPPING"); + if ($remote_retval === 1 && isset($agent_status['status']) && strtoupper($agent_status['status']) !== "OFFLINE") { + $remote_retval = -2; + } + if ($remote_retval === 1 ) { print_success(get_lang_f("server_stopped",htmlentities($home_info['home_name']))); if ($firewall_settings['status'] == "enable") @@ -176,8 +184,12 @@ function exec_ogp_module() { if ( $remote_retval === 0 ) print_failure(get_lang("agent_offline")); - elseif ( $remote_retval !== 1 ) - print_failure("Error occurred on the remote host."); + elseif ( $remote_retval !== 1 ) + { + $status_name = isset($agent_status['status']) ? strtoupper($agent_status['status']) : "UNKNOWN"; + $message = isset($agent_status['last_error']) && $agent_status['last_error'] !== "" ? $agent_status['last_error'] : "Remote stop did not verify that the process/session and game port are stopped."; + print_failure("Error occurred on the remote host. Agent status: ".htmlentities($status_name).". ".htmlentities($message)); + } } $view->refresh("?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=". $home_id . "-". $mod_id . "-" . $ip . "-" . $port,3); } diff --git a/Panel/modules/gamemanager/view_server_log.php b/Panel/modules/gamemanager/view_server_log.php index c9a2620c..b65e73f0 100644 --- a/Panel/modules/gamemanager/view_server_log.php +++ b/Panel/modules/gamemanager/view_server_log.php @@ -33,23 +33,23 @@ function exec_ogp_module() list($home_id, $mod_id, $ip, $port) = explode("-", $_GET['home_id-mod_id-ip-port']); $user_id = $_SESSION['user_id']; - - + + $isAdmin = $db->isAdmin( $user_id ); - if($isAdmin) + if($isAdmin) $home_info = $db->getGameHome($home_id); else $home_info = $db->getUserGameHome($user_id,$home_id); - - $current_mod_info = $home_info['mods'][$mod_id]; + + $current_mod_info = $home_info['mods'][$mod_id]; $home_cfg_id = $current_mod_info['home_cfg_id']; $mod_cfg_id = $current_mod_info['mod_cfg_id']; - + if($home_cfg_id === null && $mod_cfg_id === null){ print_failure(get_lang('invalid_game_mod_id')); return; } - + if ( $home_info === FALSE ) { print_failure( get_lang("no_access_to_home") ); @@ -68,7 +68,7 @@ function exec_ogp_module() $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); $home_log = ""; - + if( isset( $server_xml->console_log ) ) { $log_retval = $remote->get_log(OGP_SCREEN_TYPE_HOME, @@ -104,72 +104,50 @@ function exec_ogp_module() } } } - - // Using the refreshed class - if( isset($_GET['refreshed']) ) - { - echo "
".htmlentities($home_log)."
"; - } - else - { - echo "

".htmlentities($home_info['home_name'])."

"; - if($log_retval == 1) + + // Using the refreshed class + if( isset($_GET['refreshed']) ) { - require_once("includes/refreshed.php"); - - $control = '
- - - '; - if(isset($_GET['setInterval'])) - $control .= ""; - if(isset($_GET['view_player_commands'])) - $control .= ""; - $control .= ' "4000", - "8s" => "8000", - "30s" => "30000", - "2m" => "120000", - "5m" => "300000" ); - - $intSel = ' - - - '; - if(isset($_GET['size'])) - $intSel .= ""; - if(isset($_GET['view_player_commands'])) - $intSel .= ""; - $intSel .= get_lang("refresh_interval") . ':
"; - - $setInterval = isset($_GET['setInterval']) ? $_GET['setInterval'] : 4000; - $refresh = new refreshed(); - $pos = $refresh->add("home.php?m=gamemanager&p=log&type=cleared&refreshed&home_id-mod_id-ip-port=". $_GET['home_id-mod_id-ip-port']); - echo $refresh->getdiv($pos,"height:".$height.";overflow:auto;max-width:1600px;"); - ?>$intSel$control"; - if( ($server_xml->control_protocol and preg_match("/^r?l?con2?$/", $server_xml->control_protocol)) OR - ($server_xml->gameq_query_name and $server_xml->gameq_query_name == "minecraft") OR + $log_url = "home.php?m=gamemanager&p=log&type=cleared&refreshed&home_id-mod_id-ip-port=".rawurlencode($_GET['home_id-mod_id-ip-port']); + echo ''; + ?> + + control_protocol and preg_match("/^r?l?con2?$/", $server_xml->control_protocol)) OR + ($server_xml->gameq_query_name and $server_xml->gameq_query_name == "minecraft") OR ($server_xml->lgsl_query_name and $server_xml->lgsl_query_name == "7dtd") ) require('modules/gamemanager/rcon.php'); } diff --git a/SCHEDULER_ACTIONS_DESIGN.md b/SCHEDULER_ACTIONS_DESIGN.md new file mode 100644 index 00000000..695a586f --- /dev/null +++ b/SCHEDULER_ACTIONS_DESIGN.md @@ -0,0 +1,810 @@ +# GSP Scheduler Actions Design + +## Scope + +This is an investigation and design report only. It does not implement code. + +The goal is to redesign GSP's Scheduler / CRON feature into a safer, more useful automation system for game hosting customers and administrators. + +Repository layout reviewed: + +- `Agent-Windows` +- `Agent_Linux` (the Linux agent directory currently uses an underscore in this repository) +- `Panel` +- `Website` + +## 1. Current Scheduler Module Findings + +### Files inspected + +Panel Scheduler module: + +- `Panel/modules/cron/module.php` +- `Panel/modules/cron/navigation.xml` +- `Panel/modules/cron/cron.php` +- `Panel/modules/cron/user_cron.php` +- `Panel/modules/cron/shared_cron_functions.php` +- `Panel/modules/cron/events.php` +- `Panel/modules/cron/thetime.php` + +Panel remote/API integration: + +- `Panel/includes/lib_remote.php` +- `Panel/includes/api_functions.php` +- `Panel/modules/gamemanager/start_server.php` +- `Panel/modules/gamemanager/stop_server.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/gamemanager/update_actions.php` +- `Panel/modules/gamemanager/rcon.php` +- `Panel/modules/addonsmanager/server_content_actions.php` + +Agent scheduler implementation: + +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +### Current database tables used + +The current Scheduler module does not appear to own database tables. Module metadata has: + +- `Panel/modules/cron/module.php` +- `$db_version = 0` + +Scheduled jobs are stored on each agent in a flat file: + +- Linux: `AGENT_RUN_DIR/Schedule/scheduler.tasks` +- Linux: `AGENT_RUN_DIR/Schedule/scheduler.pid` +- Linux: `AGENT_RUN_DIR/Schedule/scheduler.log` +- Windows/Cygwin: `AGENT_RUN_DIR/scheduler.tasks` +- Windows/Cygwin: `AGENT_RUN_DIR/scheduler.pid` +- Windows/Cygwin: `AGENT_RUN_DIR/scheduler.log` + +This means the agent is currently the storage location for task definitions, and the Panel reconstructs task lists by asking each agent for its task file. + +### Current actions + +Current customer-visible scheduled actions from `get_action_selector()`: + +- `restart` +- `stop` +- `start` +- `steam_auto_update` when the game XML installer is `steamcmd` + +Additional server content actions are appended when `addonsmanager` is installed: + +- `server_content_check_updates` +- `server_content_check_workshop_updates` +- `server_content_install_updates_if_stopped` +- `server_content_install_updates_next_restart` +- `server_content_install_updates_now` +- `server_content_install_updates_and_restart` +- `server_content_notify_updates_only` +- `server_content_update_all` +- `server_content_validate_files` +- `server_content_backup_before_update` + +Admin-only raw command path: + +- `Panel/modules/cron/cron.php` exposes a second form where an admin selects a remote server and enters a raw shell command. + +### How tasks are created + +Customer task creation path: + +1. User opens `Panel/modules/cron/user_cron.php`. +2. User selects a game server and action. +3. Panel validates the five CRON fields using `checkCronInput()`. +4. Panel calls `build_cron_scheduler_command()`. +5. The command is built as a `wget` callback to `ogp_api.php`. +6. Panel sends the whole CRON line to the agent through `scheduler_add_task()`. +7. Agent appends the task line to `scheduler.tasks`. +8. Agent restarts its scheduler process. + +Admin task creation path: + +1. Admin opens `Panel/modules/cron/cron.php`. +2. Admin can use the same server/action selector. +3. Admin can also enter a raw command for a remote server. +4. Panel writes the raw command into the agent task file. + +Current scheduled API callback examples: + +```text +wget -qO- "/ogp_api.php?gamemanager/stop&token=&ip=&port=&mod_key=" --no-check-certificate > /dev/null 2>&1 +``` + +```text +wget -qO- "/ogp_api.php?server_content/run_scheduled_action&token=&home_id=&action=&options=" --no-check-certificate > /dev/null 2>&1 +``` + +### How tasks execute + +Both agents use Perl `Schedule::Cron`. + +Agent startup: + +- Stops prior scheduler process using `scheduler_stop()`. +- Creates a `Schedule::Cron` object. +- Adds a read/reload task that runs every second: + - `* * * * * *` +- Starts scheduler detached with `scheduler.pid`. + +Agent task reload: + +- `scheduler_read_tasks()` opens `scheduler.tasks`. +- It clears the in-memory timetable. +- It splits each line into five CRON fields plus command args. +- If args start with `%ACTION`, it uses `scheduler_server_action()`. +- Otherwise it adds a generic shell command task. + +Current Panel-generated jobs are generic shell commands, not `%ACTION` jobs. They execute through: + +- `scheduler_dispatcher()` +- backtick execution of the scheduled command +- append to `scheduler.log` + +The older `%ACTION=start|stop|restart` direct-agent scheduler path still exists but does not appear to be the primary current Panel path. + +### How task results are logged + +Agent logging: + +- `scheduler_log_events()` appends plain text to `scheduler.log`. +- Generic commands log: + - the command text + - any response text + +Panel viewing: + +- `Panel/modules/cron/events.php` reads `scheduler.log` from the selected remote server. +- It refreshes the log area periodically. + +Limitations: + +- No structured per-task run records. +- No status model such as pending/running/success/failed/skipped. +- No reliable per-run output attached to a task ID. +- No last run / next run / duration / exit code storage in the Panel DB. +- `wget` callbacks redirect output to `/dev/null`, so useful API responses are discarded. + +### Current limitations and bugs + +1. No Scheduler-owned database tables. +2. Tasks are stored per agent, so offline agents make task state invisible or stale. +3. Tasks contain API tokens in plain text inside agent task files. +4. Generic command scheduler can run arbitrary shell commands. +5. Admin raw command scheduling is powerful and should remain admin-only or be removed from the normal Scheduler UI. +6. Current customer tasks call the Panel through `wget`, so task execution depends on the agent reaching the Panel HTTP URL. +7. `--no-check-certificate` weakens TLS verification. +8. Task output is discarded for Panel API callbacks. +9. No retry policy. +10. No overlap prevention. +11. No conflict prevention, such as update and restart at the same time. +12. No job lock per game server. +13. No missed-run handling after agent downtime. +14. No clear timezone UX. +15. Admin and customer scheduling models are mixed in the same module. +16. Server content scheduled actions include duplicates and placeholders. +17. Some action names are not customer-friendly. +18. There is no typed argument system for warnings, backup paths, retention, RCON command allowlists, or wipe options. +19. There is no first-class notification support. +20. Linux and Windows store scheduler files in different relative locations. + +## 2. Current Action Review + +| Action | Keep/Remove/Admin Only | Why | Security Risk | Agent Support | Notes | +|---|---|---|---|---|---| +| `restart` | Keep | Core hosting feature. | Low if implemented through safe action. | Existing Panel API and agent restart support. | Should support warnings, save-world, lock, timeout, and result logging. | +| `stop` | Keep | Useful for scheduled shutdown windows and cost/resource control. | Low. | Existing Panel API and agent stop support. | Should verify stopped state through agent status. | +| `start` | Keep | Useful after maintenance windows. | Low. | Existing Panel API and agent start support. | Should show STARTING/ONLINE result, not only command fired. | +| `steam_auto_update` | Keep, rename | Useful but name is technical. | Medium due Steam credentials/update side effects. | Existing `steam_cmd` update path. | Rename to `update_server_files`; require game XML installer support. | +| `server_content_check_updates` | Keep internally, remove from customer dropdown for now | Useful as backend action but unclear to customers. | Low. | Partial Panel support. | Replace with clearer `check_content_updates`. | +| `server_content_check_workshop_updates` | Keep internally, remove from customer dropdown for now | Useful once Workshop system is mature. | Low/medium. | Partial support. | Expose later as `check_workshop_updates`. | +| `server_content_install_updates_if_stopped` | Keep internally | Safe behavior for automatic content updates. | Low. | Panel support. | Customer label should be `Update content when stopped`. | +| `server_content_install_updates_next_restart` | Keep internally | Useful queued-update pattern. | Low. | Panel support. | Needs real next-restart integration. | +| `server_content_install_updates_now` | Keep advanced customer/admin | Updates while server may be running can break files. | Medium. | Partial support. | Gate by game support and require warning. | +| `server_content_install_updates_and_restart` | Keep advanced customer/admin | Very useful but needs locking and warnings. | Medium. | Partial support. | Should become `update_mods_and_restart`. | +| `server_content_update_workshop` | Remove from dropdown; keep as internal alias | Duplicate with Workshop update action. | Medium. | Partial support. | Hide until Workshop redesign is implemented. | +| `server_content_update_all` | Remove/merge | Duplicate with install/update all. | Medium. | Partial support. | Replace with one clear `update_all_content`. | +| `server_content_notify_updates_only` | Remove for now | Name suggests notification but no notification system exists. | Low. | Partial check-only path. | Reintroduce after notifications exist. | +| `server_content_validate_files` | Keep admin/advanced | Useful repair/validate action. | Medium. | Partial support via generic script action. | Rename to `validate_content_files`; game support required. | +| `server_content_backup_before_update` | Remove or redesign | Currently sets an option but there is no clear backup implementation in that path. | Medium due false confidence. | Incomplete. | Replace with first-class backup action and update workflow option. | +| Raw remote shell command | Admin only or remove from normal UI | Powerful but dangerous. | High. | Existing generic scheduler execution. | Should not be available to customers. Should be audited if kept. | +| Legacy `%ACTION=start` | Remove/deprecate | Current Panel uses API callbacks. | Low. | Agent support exists. | Keep only during migration if old task files contain it. | +| Legacy `%ACTION=stop` | Remove/deprecate | Same as above. | Low. | Agent support exists. | Migrate to action registry. | +| Legacy `%ACTION=restart` | Remove/deprecate | Same as above. | Low. | Agent support exists. | Migrate to action registry. | + +## 3. Competitor Feature Research + +Sources reviewed: + +- Nitrado automated tasks guide: https://server.nitrado.net/guides/automated-tasks-en +- Nitrado backup guide/FAQ: https://server.nitrado.net/en-US/guides/how-to-manage-and-restore-nitrado-server-backups/ +- BisectHosting Starbase schedules: https://help.bisecthosting.com/hc/en-us/articles/40101097661083-How-to-Schedule-Tasks-on-the-Starbase-panel +- ZAP-Hosting scheduled tasks: https://zap-hosting.com/guides/docs/gameserver-scheduled-tasks +- Pterodactyl client schedules API docs: https://pterodactyl-panel.mintlify.app/api/client/schedules +- Shockbyte backups guide: https://shockbyte.com/help/knowledgebase/articles/how-to-backup-your-server-files +- PingPerfect scheduled server messages guide: https://pingperfect.com/knowledgebase/706/7-Days-to-Die--Scheduled-Server-Messages.html +- Host Havoc scheduled backups guide: https://hosthavoc.com/billing/knowledgebase/479/Creating-Scheduled-Backups.html +- GTXGaming Rust restart warning task guide: https://help.gtxgaming.co.uk/en/article/how-to-setup-restart-tasks-with-messages-for-your-rust-server-15zwnyr/ + +Common commercial features: + +- Scheduled start/stop/restart. +- Scheduled backups. +- Scheduled console/RCON commands. +- Restart warning messages. +- Task offsets within a schedule. +- Backup retention limits. +- Restart-only-if-online option. +- Manual and automatic backup creation. +- Custom task scheduling. +- Game-specific tasks such as Rust wipes or server message tools. + +Notable competitor patterns: + +- Nitrado exposes simple automated power tasks: restart, start, stop. It also has automatic backups, but docs note timezone issues and game-specific backup behavior. +- BisectHosting Starbase schedules support separate schedules and tasks, including power actions and command tasks with time offsets. +- Pterodactyl's design is strong: schedules have multiple ordered tasks, time offsets, power actions, command actions, backup actions, `only_when_online`, and continue-on-failure behavior. +- ZAP-Hosting exposes start/stop/restart, restart-if-online, create backup, and execute command, with rate limits. +- Shockbyte emphasizes scheduled backup intervals and backup slot/auto-replace retention. +- PingPerfect supports scheduled messages and Console/RCON commands for games like 7 Days to Die. +- GTXGaming documents restart warnings/countdowns for Rust. + +What GSP can do better: + +- Use typed, safe game-aware actions instead of raw commands. +- Provide prebuilt restart workflows with save-world and warning steps. +- Tie Workshop/mod updates into the Scheduler. +- Add per-task locks and conflict prevention. +- Add structured logs and visible success/failure. +- Add notifications through Discord/email/panel. +- Add game XML capability detection so users only see actions that work. +- Add maintenance windows and "run when empty" automation. +- Add resource-based triggers using existing status/resource collection work. + +## 4. Recommended GSP Scheduler Actions + +### Customer-safe actions + +- Restart server. +- Stop server. +- Start server. +- Backup server. +- Backup selected folders. +- Update Workshop mods. +- Update server content. +- Send warning message. +- Run allowed RCON command. +- Rotate logs. +- Delete old logs using admin-defined retention limits. +- Save world, if the game supports it. +- Check server status. +- Auto-restart if crashed. + +### Advanced customer actions + +- Scheduled wipe/reset for supported games. +- Validate/repair server files. +- Update SteamCMD game files. +- Clone backup to another server. +- Restore backup. +- Update mods and restart. +- Restart when player count is zero for X minutes. +- Restart if memory too high for X minutes. +- Restart if CPU stuck/high for X minutes. +- Scheduled config file replacement from approved templates. +- Scheduled database backup where applicable. + +### Admin-only actions + +- Arbitrary shell command. +- Raw script execution. +- Permission repair. +- Force kill process/session. +- Agent/node maintenance. +- Cleanup storage outside a server home. +- Clear global Workshop cache. +- Repair file ownership. +- Restart agent. +- Reboot node. +- Run panel update/maintenance. + +## 5. Proposed Action System + +Replace free-form action lists with a typed action registry. + +Each action definition should include: + +- `action_key` +- `display_name` +- `description` +- `category` +- `allowed_roles` +- `required_permissions` +- `supported_os` +- `required_agent_capability` +- `requires_game_running` +- `requires_game_stopped` +- `requires_rcon` +- `requires_workshop_support` +- `requires_steamcmd` +- `arguments_schema` +- `validation_rules` +- `timeout_seconds` +- `retry_policy` +- `overlap_policy` +- `conflict_group` +- `log_policy` +- `notification_events` + +Example: + +```yaml +scheduled_actions: + restart_server: + display_name: Restart Server + role: customer + agent_action: stop_wait_start + required_permissions: [server.power.restart] + args: + warning_minutes: + type: integer + min: 0 + max: 60 + default: 5 + warning_message: + type: string + max_length: 160 + default: "Server restart in {minutes} minutes." + save_world: + type: boolean + default: true + timeout_seconds: 600 + conflict_group: server_power + overlap_policy: skip_if_running +``` + +### Recommended schedule model + +Move from "one CRON line equals one command" to: + +- Schedule: + - name + - cron expression or interval + - timezone + - enabled + - only_when_online + - missed_run_policy + +- Tasks: + - ordered tasks within a schedule + - action key + - arguments + - time offset + - continue on failure + +This matches the strongest commercial pattern and allows: + +- 10 minutes before restart: send warning. +- 5 minutes before restart: save world. +- At restart time: restart server. +- 5 minutes after restart: send Discord notification. + +### Suggested DB tables + +`gsp_schedules` + +- `id` +- `home_id` +- `remote_server_id` +- `name` +- `cron_minute` +- `cron_hour` +- `cron_day_of_month` +- `cron_month` +- `cron_day_of_week` +- `timezone` +- `enabled` +- `only_when_online` +- `missed_run_policy` +- `max_runtime_seconds` +- `created_by` +- `created_at` +- `updated_at` + +`gsp_schedule_tasks` + +- `id` +- `schedule_id` +- `sort_order` +- `time_offset_seconds` +- `action_key` +- `arguments_json` +- `continue_on_failure` +- `enabled` +- `created_at` +- `updated_at` + +`gsp_schedule_runs` + +- `id` +- `schedule_id` +- `home_id` +- `status` +- `scheduled_for` +- `started_at` +- `finished_at` +- `duration_seconds` +- `trigger` +- `last_error` + +`gsp_schedule_task_runs` + +- `id` +- `schedule_run_id` +- `schedule_task_id` +- `action_key` +- `status` +- `started_at` +- `finished_at` +- `exit_code` +- `message` +- `error` +- `log_path` +- `output_excerpt` + +## 6. XML Integration + +Game XML should declare game-specific Scheduler support. + +Example: + +```xml + + + + + save + + + + rust_wipe + + +``` + +Global actions: + +- Start server. +- Stop server. +- Restart server. +- Backup server files. +- Rotate logs. +- Delete old backups/logs. +- Check status. + +Game-specific actions: + +- Send RCON warning. +- Save world. +- Run console command. +- Workshop update. +- Mod update. +- Wipe/reset. +- Database backup. +- Validate files. + +Actions requiring RCON: + +- Warning message. +- Save world. +- Player-count-aware empty restart if query is not enough. +- Allowed RCON command. +- Game-specific graceful shutdown. + +Actions requiring SteamCMD: + +- Update SteamCMD game files. +- Validate/repair Steam game files. + +Actions requiring Workshop support: + +- Update Workshop mods. +- Repair Workshop mods. +- Update mods and restart. + +Actions requiring backup support: + +- Backup server. +- Backup selected folders. +- Restore backup. +- Clone backup. + +## 7. Agent Integration + +### Preferred direction + +The agent should execute typed scheduled actions, not raw customer shell text. + +New agent methods could be: + +- `scheduler_action_start(home_id, action_manifest_json)` +- `scheduler_action_status(home_id, action_run_id)` +- `scheduler_action_log(home_id, action_run_id, offset)` +- `scheduler_action_cancel(home_id, action_run_id)` + +The Panel should store schedules and send due actions to agents, or the agent should receive a structured schedule manifest from Panel. The cleanest long-term design is Panel-owned schedules plus an agent-side runner for actions. + +### Start/stop/restart + +Agent should: + +- Use existing start/stop/restart functions. +- Use the new agent status model as source of truth. +- Wait for state transitions. +- Return structured result. + +Restart should: + +1. Optional RCON warning. +2. Optional save-world. +3. Stop. +4. Wait configured seconds. +5. Start. +6. Poll until STARTING/ONLINE/UNRESPONSIVE. + +### Backup + +Agent should: + +- Create compressed archives through a typed backup action. +- Support include/exclude folders from safe config. +- Store backup manifests. +- Enforce retention. +- Avoid backing up transient cache/log folders unless configured. + +### Update + +Agent should: + +- Run SteamCMD update or server content update through typed job actions. +- Avoid overlapping update with running backup/restart. +- Mark restart required when applicable. + +### RCON/console command + +Agent should: + +- Use existing `send_rcon_command` support. +- Validate commands against action rules. +- Log command and response. +- Redact credentials. + +Customer-safe RCON should use templates: + +- `say {message}` +- `save` +- `save-all` +- game-specific warning command + +Raw RCON text should be advanced/admin controlled. + +### Mod update + +Agent should: + +- Run Workshop/server-content job runner from the Workshop design. +- Return job status and logs. +- Mark restart required. + +### Log cleanup + +Agent should: + +- Delete only configured log paths. +- Enforce age/size limits. +- Log every removed path count/bytes. + +### Status/resource actions + +Agent should: + +- Check process/session/port status. +- Optionally check memory/CPU samples. +- Execute conditional restart only after threshold duration. + +### Timeouts and failure reporting + +Every action should have: + +- timeout +- retry count +- retry delay +- result status +- error message +- log excerpt +- correlation ID + +## 8. Task Logs and User Feedback + +Recommended run statuses: + +- `pending` +- `running` +- `success` +- `failed` +- `skipped` +- `canceled` +- `timed_out` + +The UI should show: + +- schedule name +- enabled/disabled +- next run time +- last run time +- last status +- last duration +- current running task +- output log +- error message +- retry count + +Run details should show: + +- each task in the schedule +- action arguments summary +- start time +- finish time +- result +- output/log + +Do not rely only on `scheduler.log`. + +## 9. Notifications + +Supported notification channels: + +- Panel notification. +- Email. +- Discord webhook. +- Generic webhook later. + +Notification events: + +- Before restart. +- After restart. +- Backup succeeded. +- Backup failed. +- Update available. +- Update installed. +- Task skipped because server was offline/running. +- Task failed. +- Disk retention cleanup ran. + +Security: + +- Webhook URLs must be stored securely. +- Do not expose tokens in task logs. +- Customers should not be able to send arbitrary webhooks from shared infrastructure unless allowed by policy. + +Pre-restart warning types: + +- RCON in-game message. +- Console command. +- Discord/webhook message. +- Panel notification. + +## 10. Implementation Phases + +### Phase 1: Inventory/report only + +- Complete this report. +- Do not modify code. + +### Phase 2: Remove or hide useless actions + +- Hide duplicate server-content actions from customer dropdown. +- Keep internal aliases for backward compatibility. +- Hide `server_content_backup_before_update` until real backup exists. +- Keep raw remote command admin-only. + +### Phase 3: Safe action registry + +- Add PHP action registry. +- Define roles, permissions, arguments, validation, and display names. +- Replace hardcoded dropdown arrays. + +### Phase 4: Task logging + +- Add schedule/task/run tables. +- Store run status and results. +- Keep agent `scheduler.log` as low-level debug only. + +### Phase 5: Restart/backup/update actions + +- Implement typed restart with warning/save-world hooks. +- Implement first-class server backup action. +- Implement update server files action. + +### Phase 6: RCON warnings + +- Add game XML `scheduler_support`. +- Add allowed warning/save commands. +- Add command templates and validation. + +### Phase 7: Workshop update integration + +- Integrate with the redesigned Workshop/server-content job system. +- Add update mods and update mods then restart workflows. + +### Phase 8: Notifications + +- Add panel notifications. +- Add Discord webhook. +- Add email. + +### Phase 9: Commercial polish + +- Multi-task schedules with offsets. +- Clone schedule to another server. +- Maintenance window mode. +- Conditional empty-server restart. +- Resource threshold triggers. +- Missed-run handling. +- Conflict and overlap visualization. + +## 11. Final Recommendation + +### Remove or hide + +- Hide raw server-content internal actions from customer dropdown. +- Remove customer-facing `server_content_notify_updates_only` until notifications exist. +- Remove customer-facing `server_content_backup_before_update` until backup is real. +- Merge duplicate update actions into clear labels. +- Deprecate legacy `%ACTION=` task format after migration. + +### Keep + +- Start server. +- Stop server. +- Restart server. +- SteamCMD update, renamed to `Update server files`. +- Server content / Workshop update, once the Workshop system is mature. +- Admin raw command only behind explicit admin permissions. + +### Build first + +1. Typed action registry. +2. DB-backed schedules and run logs. +3. Restart server with warning and optional save-world. +4. Backup server with retention. +5. Update server files. +6. Update Workshop mods. +7. Notifications. + +### Admin-only + +- Shell command. +- Raw script execution. +- Force kill. +- Permission repair. +- Node cleanup. +- Agent restart/reboot. + +### Delay until later + +- Resource-triggered restarts. +- Wipe/reset workflows. +- Restore backup scheduling. +- Clone schedules. +- Generic webhooks. +- Advanced conditional schedules. + +## Summary + +The current GSP Scheduler is functional but primitive. It stores CRON lines on agents, executes shell commands, and often calls back into the Panel through `wget`. That makes it flexible, but it does not provide the safety, visibility, or polish expected from a modern commercial game hosting panel. + +The recommended path is a typed, DB-backed schedule system with safe action definitions, game XML capability flags, agent-side action execution, structured run logs, notifications, and first-class workflows for restart, backup, update, Workshop mods, and RCON warnings. + diff --git a/STEAM_WORKSHOP_DESIGN.md b/STEAM_WORKSHOP_DESIGN.md new file mode 100644 index 00000000..cd092d7e --- /dev/null +++ b/STEAM_WORKSHOP_DESIGN.md @@ -0,0 +1,1396 @@ +# GSP Steam Workshop and Server Content Design + +## Scope + +This document is an investigation and design report only. It does not implement code. + +The goal is a professional Steam Workshop and general server content system for GSP that works across: + +- `Agent-Windows` +- `Agent_Linux` (the Linux agent directory currently uses an underscore in this repository) +- `Panel` +- `Website` + +The system should support games that use Steam Workshop directly, games that require server-side mod installation, and games that use non-Workshop add-ons or content packs. + +Target games include DayZ, Arma / Arma 2 / Arma 3, Rust, Garry's Mod, CS / Source games where applicable, and future games with installable server content. + +## Current Code Findings + +### Add-ons manager state + +The old add-ons manager has already been partially converted into a broader Server Content Manager. + +Relevant files: + +- `Panel/modules/addonsmanager/addons_manager.php` +- `Panel/modules/addonsmanager/addons_installer.php` +- `Panel/modules/addonsmanager/server_content_helpers.php` +- `Panel/modules/addonsmanager/server_content_actions.php` +- `Panel/modules/addonsmanager/workshop_content.php` +- `Panel/modules/addonsmanager/workshop_action.php` +- `Panel/modules/addonsmanager/server_content_categories.php` +- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh` +- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh` +- `Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md` +- `Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md` + +What exists: + +- Server Content language and categories have started replacing legacy "add-ons" language. +- Install methods include `download_zip`, `steam_workshop`, `config_edit`, and `post_script`. +- Workshop content has a user-facing page where customers can enter Workshop item IDs. +- Workshop IDs are validated as numeric strings. +- A `server_content_workshop` table is created/migrated by helper code. +- A remote manifest is written under the game server home. +- Bundled generic Linux and Cygwin/Windows scripts are copied to the agent and executed. +- The current scripts can call SteamCMD with `+workshop_download_item`. +- Basic install, update, update selected, update all, and remove selected actions exist. +- Actions are logged through the panel logger and the script writes a text log under `gsp_server_content`. + +What is incomplete: + +- No search or metadata lookup workflow exists in the UI. +- Titles are stored but not reliably resolved. +- There is no load order management. +- There is no enabled/disabled state separate from installed/removed state. +- There is no per-game install strategy model. +- There is no first-class DayZ/Arma `-mod=` generation. +- There is no robust copy-key behavior for DayZ `.bikey` files. +- There is no clone/copy mod list workflow. +- There is no asynchronous job/progress system for long Workshop installs. +- Current generic scripts are synchronous from the Panel request. +- Current scripts assume anonymous SteamCMD login. +- Current scripts only constrain writes under the server home, so they do not support a true agent global cache outside the game directory. +- Current scripts run optional `post_install_script` using `bash -lc`, which should remain admin-only and should not be customer-provided. +- The XML schema does not yet define the Workshop support tags that the helper code expects. + +### Current Workshop Phase 1 flow + +The current user flow is: + +1. User opens the Workshop Mods page for a server. +2. User enters numeric Workshop item IDs. +3. Panel inserts or updates rows in `server_content_workshop`. +4. Panel writes a manifest to: + - `{GAME_HOME}/gsp_server_content/workshop_manifest.json` +5. Panel copies a bundled script to: + - `{GAME_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh` + - or `{GAME_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh` +6. Panel executes: + - `bash ` +7. The script uses SteamCMD: + - `+force_install_dir ` + - `+login anonymous` + - `+workshop_download_item validate` +8. The script copies downloaded files to a target folder, defaulting to: + - `{SERVER_ROOT}/@{WORKSHOP_ID}` +9. Panel updates DB row state to `installed`, `failed`, or `removed`. + +This is useful as a proof of concept, but it is not yet a commercial Workshop/mod system. + +### Current DB/storage state + +The current helper creates: + +`server_content_workshop` + +Known columns: + +- `id` +- `content_id` +- `home_id` +- `home_cfg_id` +- `remote_server_id` +- `workshop_app_id` +- `workshop_item_id` +- `title` +- `install_state` +- `last_installed_at` +- `last_updated_at` +- `last_error` +- `created_by` +- `created_at` +- `updated_at` + +The current helper also writes agent-side files under the server home: + +- `gsp_server_content/workshop_manifest.json` +- `gsp_server_content/manifests/*.json` +- `gsp_server_content/installed_content.json` +- `gsp_server_content/workshop_install.log` +- `gsp_server_content/workshop_install_windows.log` +- `gsp_server_content/workshop/removed/*` +- `gsp_server_content/scripts/workshop/*` + +Current DB weaknesses: + +- No load order column. +- No enabled column. +- No install path column per row. +- No install folder/name column per row. +- No installed Workshop timestamp/version/hash metadata from Steam. +- No job table for long-running installs. +- No normalized global Workshop item metadata table. +- No clean split between desired state, installed state, and job state. + +### Current Panel-Agent communication + +Panel communicates with agents through XML-RPC using `Panel/includes/lib_remote.php`. + +Relevant methods: + +- `steam_cmd(...)` +- `steam_workshop(...)` +- `get_workshop_mods_info(...)` +- `start_file_download(...)` +- `remote_writefile(...)` +- `remote_readfile(...)` +- `exec(...)` + +There are two Workshop paths: + +1. Legacy agent RPC: + - Panel method: `OGPRemoteLibrary::steam_workshop(...)` + - Agent subroutine: `steam_workshop_without_decrypt(...)` + - Runs SteamCMD in a screen update session. + - Generates post-install shell snippets that edit a config file. + +2. Current Server Content manifest path: + - Panel writes a JSON manifest using `remote_writefile`. + - Panel copies an approved script to the agent using `remote_writefile`. + - Panel runs the script through `exec`. + - This path does not use the old `steam_workshop` RPC. + +The second path is cleaner for future Server Content work because it is manifest-driven, but it should be moved away from direct synchronous `exec` into a first-class agent job/action. + +### Current agent SteamCMD behavior + +Linux agent: + +- File: `Agent_Linux/ogp_agent.pl` +- SteamCMD path constants: + - `AGENT_RUN_DIR/steamcmd` + - `steamcmd.sh` +- `check_steam_cmd_client` downloads and installs SteamCMD if missing. +- `steam_cmd_without_decrypt` creates a SteamCMD runscript and runs it in a screen update session. +- `steam_workshop_without_decrypt` creates a SteamCMD runscript containing `workshop_download_item` lines and runs it in a screen update session. +- `get_workshop_mods_info` reads `.ogpmod` files from `AGENT_RUN_DIR/WorkshopModsInfo`. + +Windows/Cygwin agent: + +- File: `Agent-Windows/ogp_agent.pl` +- SteamCMD path constants: + - `/OGP/steamcmd` + - `steamcmd.exe` +- `steam_cmd_without_decrypt` uses Cygwin path conversion for `force_install_dir`. +- `steam_workshop_without_decrypt` uses Cygwin path conversion for the mods path and the SteamCMD runscript. +- `get_workshop_mods_info` mirrors the Linux behavior. + +Agent limitations: + +- The legacy `steam_workshop` RPC is not tied to the newer `server_content_workshop` DB table. +- It edits config files through generated shell script content instead of structured startup parameter state. +- It does not understand enabled/disabled state or load order. +- It does not expose a durable install job ID with pollable progress. +- It does not track per-server install manifests as the source of truth. +- It does not provide a DayZ-specific strategy. + +### Current game XML and startup parameter behavior + +Relevant files: + +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/config_servers.php` +- `Panel/modules/config_games/cli-params.php` +- `Panel/modules/gamemanager/mini_start.php` +- `Panel/modules/gamemanager/cfg_text_replace.php` +- `Panel/modules/gamemanager/update_actions.php` + +The XML schema currently supports: + +- `cli_template` +- `cli_params` +- `replace_texts` +- `custom_fields` +- `pre_install` +- `post_install` +- `pre_start` +- `post_start` +- `environment_variables` +- `lock_files` +- `configuration_files` + +The XML schema does not currently support a first-class `workshop_support` block. Helper code already checks ad hoc fields such as: + +- `workshop_app_id` +- `workshop_appid` +- `steam_workshop_app_id` +- `steam_workshop_appid` +- `workshop_script_linux` +- `workshop_script_windows` + +Because `read_server_config()` validates XML against `schema_server_config.xml`, these ad hoc fields are not safe to use in game XML until the schema is extended. + +Startup parameters are currently generated in `mini_start.php` from `cli_template` and `cli_params`, with stored server values/custom fields layered in. This should be the integration point for generated mod parameters, but the mod list must be structured data, not raw customer command text. + +### Current config/custom field behavior + +`cfg_text_replace.php` reads custom fields from the database and applies configured replacements to files through `remote_readfile` and `remote_writefile`. + +This is useful for game config editing, but it should not be used as the primary Workshop/mod list system because: + +- It is text replacement based. +- It does not model enabled/disabled mods. +- It does not model load order. +- It does not model install state. +- It can be fragile for repeated add/remove/reorder operations. + +### Existing Workshop support in game configs + +Some Source/Garry's Mod configs expose Workshop-related startup params directly: + +- `+host_workshop_collection` +- `+host_workshop_map` +- `+workshop_start_map` + +This is game-managed Workshop support. GSP should preserve that behavior and not force all games through server-side file copying. + +## Current Add-ons/Content Module State + +The current module is best described as "Server Content Phase 1." + +Partially working pieces: + +- Admins can define content records in the `addons` table. +- Users can view server content from the add-ons manager. +- `steam_workshop` is recognized as an install method. +- Workshop IDs can be entered manually. +- A manifest-driven script runner exists. +- Basic install/update/remove states exist. + +Broken or incomplete pieces for commercial use: + +- No schema-level game capability declaration. +- No game-specific install strategy selection. +- No load order UI. +- No enable/disable UI. +- No safe startup parameter generation for mod lists. +- No DayZ/Arma key copy strategy. +- No metadata discovery or title resolution. +- No async job/progress model. +- No central cache design. +- No cache cleanup policy. +- No rollback/repair strategy. +- No robust uninstall ownership model for copied keys/files. +- No support for private Workshop items requiring login beyond older SteamCMD update settings. + +## Recommended Architecture + +### Main design rule + +The Panel database should be the source of truth for desired server content state. + +The agent should be the source of truth for execution state and filesystem results. + +Agent files should be treated as runtime manifests, logs, caches, and recovery aids, not as the only source of truth. + +### Core model + +Create a first-class Server Content / Workshop system with: + +- XML-declared game capabilities. +- Database-stored per-server content state. +- Agent-executed install/update/uninstall jobs. +- Structured startup parameter generation. +- Game-specific install strategies. +- Safe, pollable job logs. +- Optional global cache. +- Cross-platform behavior for Linux and Cygwin/Windows agents. + +### Required states + +For a server Workshop item: + +- `selected`: saved but not installed. +- `queued`: job has been queued. +- `installing`: agent is installing it. +- `installed`: installed and available. +- `disabled`: installed but excluded from generated startup parameters. +- `updating`: update job is running. +- `failed`: last install/update failed. +- `removing`: uninstall job is running. +- `removed`: removed from desired state. + +For an install job: + +- `queued` +- `running` +- `succeeded` +- `failed` +- `cancelled` + +### Recommended component responsibilities + +Panel: + +- Reads game XML capability definitions. +- Shows Workshop/Mods/Server Content UI only when supported. +- Stores desired content state. +- Queues install/update/remove/reorder actions. +- Generates desired startup mod list from DB state. +- Sends structured manifests/jobs to the agent. +- Polls agent for progress/logs. +- Shows clear error messages and restart-required prompts. + +Agent: + +- Runs SteamCMD. +- Stages downloads. +- Copies/installs files using approved strategies. +- Copies DayZ/Arma keys when configured. +- Writes progress logs. +- Writes per-job result JSON. +- Tracks installed file manifests where useful. +- Does not trust customer-supplied shell commands. +- Does not decide desired load order. + +Game XML: + +- Declares whether Workshop/content support exists. +- Declares the provider, Steam app IDs, install strategy, path templates, startup parameter format, key copy behavior, cache policy, and authentication needs. +- Provides admin-defined strategy data, not customer-editable commands. + +## XML Capability Proposal + +Add a first-class optional block to `schema_server_config.xml`. + +Example: + +```xml + + 1 + steam + 221100 + 221100 + steamcmd + dayz_mod_folder + {GAME_PATH} + @{SAFE_TITLE} + -mod={MOD_LIST} + ; + @ + 1 + 1 + 1 + 1 + 0 + + {MOD_PATH}/keys/*.bikey + {GAME_PATH}/keys + + +``` + +### Required XML fields + +- `enabled` +- `provider` +- `download_method` +- `install_strategy` + +For Steam Workshop: + +- `steam_app_id` +- `workshop_app_id` + +For strategies that install files: + +- `install_path` + +For strategies that update startup parameters: + +- `startup_param_format` +- `mod_separator` + +### Optional XML fields + +- `mod_prefix` +- `mod_folder_format` +- `server_mod_param_format` +- `client_mod_param_format` +- `copy_keys` +- `requires_restart` +- `supports_load_order` +- `supports_disable` +- `allow_cache` +- `cache_scope` +- `requires_steam_login` +- `steam_login_profile` +- `max_items` +- `allowed_item_types` +- `install_script_key` +- `post_install_strategy` +- `validate_files` +- `backup_before_update` +- `preserve_previous_version` + +### Supported install strategies + +- `game_managed_workshop` +- `steamcmd_download_only` +- `copy_to_game_root` +- `copy_to_mod_folder` +- `dayz_mod_folder` +- `arma_mod_folder` +- `config_only` +- `custom_scripted_install` + +### Strategy meanings + +`game_managed_workshop` + +- The game downloads/uses Workshop content internally. +- GSP should generate game-supported startup params such as Workshop collection IDs. +- No server-side file copy is required. +- Example: Garry's Mod or Source games using `+host_workshop_collection`. + +`steamcmd_download_only` + +- Agent downloads Workshop item to a known cache/staging path. +- No direct install into game root. +- Useful for games/tools that consume downloaded content separately. + +`copy_to_game_root` + +- Agent downloads then copies files directly under `{GAME_PATH}` or a safe subpath. + +`copy_to_mod_folder` + +- Agent downloads then copies content to a generated mod folder. + +`dayz_mod_folder` + +- Agent downloads content. +- Agent installs under `{GAME_PATH}/@ModName`. +- Agent copies `.bikey` files to `{GAME_PATH}/keys`. +- Panel generates `-mod=@Mod1;@Mod2` from enabled ordered items. + +`arma_mod_folder` + +- Similar to DayZ, but should support Arma-specific client/server mod parameter variants as needed. + +`config_only` + +- The content changes structured config values only. +- No Workshop download. + +`custom_scripted_install` + +- Admin-defined trusted script/action. +- Should be used sparingly. +- Customer must never provide arbitrary script text. + +### XML validation rules + +- `enabled` must be `0` or `1`. +- App IDs and Workshop IDs must be numeric. +- Strategy must be one of the allowed strategy names. +- Path templates must only use approved variables. +- Path templates must resolve under allowed roots. +- Startup param format must only contain approved placeholders. +- `copy_keys` target path must resolve under the game home unless explicitly allowed by admin policy. +- `custom_scripted_install` must reference an approved script key/path, not inline customer text. + +### Backward compatibility + +Older XML files should continue to work with no Workshop support. + +If `workshop_support` is absent: + +- Existing startup parameters such as `+host_workshop_collection` continue to work. +- Existing add-ons manager content can still be shown if admin-created content records exist. +- No Workshop Mods page should be shown unless enabled by admin record or capability detection. + +Existing ad hoc fields such as `workshop_app_id` should either be migrated into `workshop_support` or tolerated by a compatibility parser after the schema is explicitly updated. + +## DB Table Proposal + +### `gsp_workshop_items` + +Global metadata cache for Workshop items. + +Suggested columns: + +- `id` +- `provider` +- `app_id` +- `workshop_id` +- `title` +- `description_short` +- `preview_url` +- `author_name` +- `visibility` +- `file_size` +- `time_created` +- `time_updated` +- `metadata_json` +- `last_metadata_fetch_at` +- `last_metadata_error` +- `created_at` +- `updated_at` + +Unique key: + +- `(provider, app_id, workshop_id)` + +### `gsp_server_workshop_items` + +Desired per-server Workshop state. + +Suggested columns: + +- `id` +- `home_id` +- `home_cfg_id` +- `remote_server_id` +- `content_id` +- `provider` +- `app_id` +- `workshop_app_id` +- `workshop_id` +- `item_title` +- `install_name` +- `safe_folder_name` +- `install_path` +- `enabled` +- `load_order` +- `install_strategy` +- `install_state` +- `installed_version` +- `installed_timestamp` +- `last_update_check` +- `last_installed_at` +- `last_updated_at` +- `last_error` +- `restart_required` +- `installed_by` +- `created_at` +- `updated_at` + +Recommended unique key: + +- `(home_id, provider, workshop_id)` + +### `gsp_workshop_install_jobs` + +Pollable install/update/remove jobs. + +Suggested columns: + +- `id` +- `job_uuid` +- `home_id` +- `remote_server_id` +- `action` +- `status` +- `requested_by` +- `started_at` +- `finished_at` +- `agent_job_id` +- `manifest_path` +- `log_path` +- `result_path` +- `progress_percent` +- `last_message` +- `last_error` +- `created_at` +- `updated_at` + +### `gsp_workshop_install_job_items` + +Per-item job details. + +Suggested columns: + +- `id` +- `job_id` +- `server_workshop_item_id` +- `workshop_id` +- `action` +- `status` +- `install_path` +- `message` +- `error` +- `started_at` +- `finished_at` + +### `gsp_server_content_profiles` + +Optional admin-defined reusable profiles per game/config. + +Suggested columns: + +- `id` +- `home_cfg_id` +- `profile_key` +- `display_name` +- `provider` +- `steam_app_id` +- `workshop_app_id` +- `install_strategy` +- `install_path_template` +- `startup_param_format` +- `copy_keys_json` +- `cache_policy` +- `created_at` +- `updated_at` + +### Source of truth + +The Panel DB should be the source of truth for: + +- Which mods are desired. +- Which mods are enabled. +- Load order. +- Generated startup params. +- Last known install state. + +The agent should write runtime state for: + +- Active job progress. +- Install logs. +- Download/cache manifests. +- Files installed during a job. +- Last job result. + +## Agent Execution Proposal + +### Preferred direction + +Add a dedicated agent action for server content jobs instead of running generic `exec` synchronously from a web request. + +Potential XML-RPC methods: + +- `server_content_start_job(home_id, manifest_json)` +- `server_content_job_status(home_id, job_id)` +- `server_content_job_log(home_id, job_id, offset)` +- `server_content_cancel_job(home_id, job_id)` + +The agent should return quickly with a job ID. The Panel polls status and logs. + +### Manifest shape + +Example: + +```json +{ + "manifest_version": 2, + "job_uuid": "uuid", + "home_id": 123, + "home_path": "/home/ogp_agent/OGP_User_Files/123", + "provider": "steam", + "action": "install_update", + "strategy": "dayz_mod_folder", + "steam_app_id": "221100", + "workshop_app_id": "221100", + "startup": { + "param_format": "-mod={MOD_LIST}", + "separator": ";", + "mod_prefix": "@" + }, + "copy_keys": { + "enabled": true, + "source_pattern": "{MOD_PATH}/keys/*.bikey", + "target_path": "{GAME_PATH}/keys" + }, + "items": [ + { + "workshop_id": "1559212036", + "title": "CF", + "safe_folder_name": "@CF", + "enabled": true, + "load_order": 10, + "install_path": "{GAME_PATH}/@CF" + } + ] +} +``` + +### Agent job execution + +For each job: + +1. Validate manifest fields. +2. Resolve paths from approved templates only. +3. Create job directory under agent control. +4. Start a background worker, preferably screen-backed for consistency with existing agent behavior. +5. Write progress logs. +6. Run SteamCMD download. +7. Verify expected download path exists. +8. Stage content. +9. Install/copy/sync to final path. +10. Copy keys if strategy requires it. +11. Write installed file manifest. +12. Write job result JSON. +13. Return status to Panel polling. + +### SteamCMD command + +Baseline command: + +```bash +steamcmd +login anonymous +workshop_download_item validate +quit +``` + +Authenticated variant: + +```bash +steamcmd +login +workshop_download_item validate +quit +``` + +The agent must redact credentials from logs. + +### Agent paths + +Recommended paths: + +- Job root: + - `{CONTROL_PATH}/server_content/jobs/{job_uuid}` +- Job manifest: + - `{CONTROL_PATH}/server_content/jobs/{job_uuid}/manifest.json` +- Job log: + - `{CONTROL_PATH}/server_content/jobs/{job_uuid}/job.log` +- Job result: + - `{CONTROL_PATH}/server_content/jobs/{job_uuid}/result.json` +- Per-server content manifest: + - `{CONTROL_PATH}/server_content/installed_manifest.json` +- Per-server staging: + - `{CONTROL_PATH}/server_content/staging/{job_uuid}` +- Per-server Workshop staging: + - `{CONTROL_PATH}/server_content/workshop/staging/{workshop_id}` + +`CONTROL_PATH` should be agent-managed and should not be customer-editable through FTP/file manager. + +### Cache paths + +Optional global cache: + +- `{AGENT_CACHE}/steam_workshop/{workshop_app_id}/{workshop_id}` + +Optional per-server cache: + +- `{CONTROL_PATH}/server_content/cache/steam_workshop/{workshop_app_id}/{workshop_id}` + +The first version can use per-server staging only. Global cache can be added once ownership, quota, cleanup, and sharing rules are defined. + +## User Workflow + +Customer workflow: + +1. Open a game server. +2. Click `Workshop`, `Mods`, or `Server Content`. +3. Paste a Workshop URL or numeric Workshop ID. +4. Panel extracts and validates the numeric ID. +5. Panel optionally fetches title/metadata. +6. User adds the item to the install queue. +7. User chooses enabled or disabled. +8. User adjusts load order where the game supports it. +9. User clicks `Install / Update`. +10. Panel shows live progress and logs. +11. When complete, Panel marks restart required if the game is running. +12. User restarts server. +13. Startup params are generated from the enabled ordered mod list. + +Installed mod list should show: + +- Enabled checkbox/toggle. +- Load order controls. +- Workshop ID. +- Title. +- Folder/install name. +- Installed status. +- Last updated. +- Last error. +- Actions: update, repair, disable, uninstall. + +## Admin Workflow + +Admin workflow: + +1. Edit game XML or admin content profile. +2. Enable Workshop support. +3. Set provider and Steam app/workshop IDs. +4. Select install strategy. +5. Define install path template. +6. Define startup parameter format. +7. Define key copy behavior. +8. Choose cache policy. +9. Choose whether Steam login is required. +10. Save and validate schema. +11. Test install/update/uninstall on Linux and Cygwin/Windows agents. + +Admins should not need to write shell scripts for common games like DayZ, Arma, Rust, Garry's Mod, or Source games. + +## Workshop Item Discovery + +### First version + +Support: + +- Numeric Workshop ID entry. +- Steam Workshop URL paste. +- Extraction of the `id=` query parameter. +- Basic local validation. +- Optional metadata fetch when a Steam Web API key is configured. + +Accepted examples: + +- `1559212036` +- `https://steamcommunity.com/sharedfiles/filedetails/?id=1559212036` +- Multiple IDs separated by newline or comma. + +Validation: + +- Extract only numeric IDs. +- Reject non-numeric values. +- Deduplicate IDs. +- Enforce optional max item count per game/server. + +### Professional version + +Support: + +- Steam Web API metadata fetch. +- Cached metadata table. +- Search by title when API key is configured. +- Preview image/title/author display. +- Visibility and removed/private item warnings. +- Update timestamp checks. + +Limitations: + +- Some Workshop content requires authenticated Steam credentials. +- Some items are private, age-gated, removed, or region-restricted. +- Search can be rate-limited. +- SteamCMD may download content even when metadata fetch fails, or vice versa. + +Recommendation: + +- Build the first version around URL/ID entry. +- Add metadata enrichment as a progressive enhancement. +- Do not block install solely because metadata is unavailable. + +## Download and Install Strategy + +### SteamCMD execution + +SteamCMD should run on the agent host because the agent has filesystem access to the game server install. + +Linux: + +- Use `steamcmd.sh`. +- Existing agent already knows `AGENT_RUN_DIR/steamcmd/steamcmd.sh`. + +Cygwin/Windows: + +- Use `steamcmd.exe`. +- Convert Cygwin paths to Windows paths when passing paths to SteamCMD. +- Keep scripts and screen usage as similar to Linux as possible. + +### Download flow + +Recommended flow: + +1. Resolve SteamCMD path. +2. Resolve download/cache path. +3. Run `workshop_download_item`. +4. Capture stdout/stderr to job log. +5. Verify expected content folder exists. +6. Stage content. +7. Install via strategy. +8. Record installed file manifest. + +### DayZ install flow + +For each enabled mod: + +1. Download Workshop item with app ID `221100`. +2. Resolve safe display title if available. +3. Generate safe folder name: + - Prefer `@Title` normalized for filesystem and startup use. + - Fall back to `@`. +4. Copy content to: + - `{GAME_PATH}/@ModName` +5. Copy `.bikey` files from common locations: + - `{MOD_PATH}/keys/*.bikey` + - `{MOD_PATH}/Keys/*.bikey` + - optionally recursive search if configured. +6. Copy keys to: + - `{GAME_PATH}/keys` +7. Track copied keys in an installed file manifest. +8. Generate `-mod=@Mod1;@Mod2` from enabled ordered items. +9. Mark restart required if server is running. + +### Generic game support + +Game-managed Workshop games: + +- Store Workshop collection/map IDs. +- Generate the game's native Workshop startup params. +- Do not copy files unless configured. + +Copy-to-folder games: + +- Download Workshop item. +- Copy folder contents to a configured target path. +- Optionally add startup parameters. + +Config-only content: + +- Apply structured config changes from approved templates. + +Custom scripted install: + +- Admin-defined trusted scripts only. +- Scripts should receive a manifest path. +- Scripts should write structured result JSON. + +## Startup Parameter Integration + +Startup parameter generation must be structured and repeatable. + +For DayZ/Arma style mods: + +1. Load enabled Workshop items for the server ordered by `load_order`. +2. Build `MOD_LIST` using the XML `mod_separator`. +3. Render `startup_param_format`. +4. Inject the rendered mod parameter into startup generation. +5. Avoid duplicate `-mod=` entries. +6. Preserve unrelated startup parameters. +7. Remove generated mod params when all mods are disabled/uninstalled. + +Example: + +```text +-mod=@CF;@Dabs Framework;@VPPAdminTools +``` + +Rules: + +- Customers should not edit the raw generated `-mod=` string. +- Customers may reorder/enable/disable structured mod rows. +- Generated startup fragments should be labeled internally as GSP-managed. +- Existing custom startup params should remain separate where possible. + +Recommended implementation: + +- Add a startup fragment resolver before `mini_start.php` finalizes `$start_cmd`. +- The resolver reads the server content DB state and XML `workshop_support`. +- It returns a generated fragment such as `-mod=...`. +- The fragment is inserted through a reserved placeholder or appended according to XML rules. + +Possible XML placeholder: + +```xml +%GAME_TYPE% -config=serverDZ.cfg {GSP_WORKSHOP_PARAMS} +``` + +If no placeholder exists, XML can specify: + +```xml + +``` + +## Uninstall, Disable, Update, Repair + +### Disable + +Disable should: + +- Set `enabled=0`. +- Keep installed files. +- Remove the item from generated startup parameters. +- Mark restart required if server is running. + +### Uninstall + +Uninstall should: + +- Set job status to `removing`. +- Remove item from startup mod list. +- Stop using the mod immediately after next restart. +- Optionally delete installed files. +- Optionally leave global cache intact. +- Remove copied keys only if safe. +- Update DB row status. + +Key cleanup rule: + +- Only remove keys that GSP knows it copied and that are not still required by another installed mod. +- If ownership is unclear, leave the key and warn the user/admin. + +### Update + +Update should: + +- Download new Workshop content. +- Stage it separately. +- Preserve old installed folder until staging succeeds. +- Swap/copy into place only after successful download. +- Roll back if install fails where feasible. +- Mark restart required if server is running. + +### Repair/reinstall + +Repair should: + +- Re-run download with `validate`. +- Re-copy content. +- Re-copy keys. +- Rebuild startup parameters. + +### Clone mod set + +Clone should: + +- Copy desired mod list, enabled state, and load order to another compatible server. +- Validate same game/config capability. +- Queue install/update job for the target server. +- Reuse cache when safe and available. + +## Caching and Cleanup Plan + +### Cache options + +No cache: + +- Lowest complexity. +- Highest repeated bandwidth usage. + +Per-server cache: + +- Safer ownership model. +- Easier cleanup. +- Less sharing benefit. + +Agent global cache: + +- Best reuse across servers. +- Saves bandwidth and time. +- Requires quota, permissions, locking, and cleanup. + +### Recommended phases + +First implementation: + +- Use per-server staging/cache under agent control. +- Do not expose cache through customer file manager. + +Later: + +- Add optional global cache by app ID and Workshop ID. +- Track size and last-used timestamp. +- Admin-configurable max cache size. +- Cleanup least recently used items. +- Support hardlink/symlink/junction only after cross-platform testing. + +### Cache metadata + +Cache manifest should track: + +- Provider. +- App ID. +- Workshop ID. +- Download timestamp. +- Steam update timestamp if known. +- Size. +- Source path. +- Last used by home ID. +- Last validation result. + +## Security Considerations + +### Customer input + +Customers may provide: + +- Workshop URLs. +- Workshop IDs. +- Enable/disable choices. +- Load order. + +Customers must not provide: + +- Shell commands. +- SteamCMD command fragments. +- Arbitrary install paths. +- Arbitrary startup command fragments. +- Arbitrary copy patterns. + +### Command safety + +- Commands should be generated from trusted XML/admin config and validated DB state. +- Workshop IDs must be numeric. +- URL parsing must only extract numeric `id` values. +- Path templates must resolve under allowed roots. +- No path traversal. +- No null bytes or control characters. +- Logs must redact Steam credentials. +- Admin scripts must be explicitly marked trusted. + +### File location safety + +Managed manifests, job files, logs, and scripts should live outside customer-editable roots where possible. + +If current deployment requires storing under game home temporarily: + +- Use a `gsp_server_content` directory. +- Prevent customer file manager/FTP access to that control directory if possible. +- Validate that all write targets stay under approved paths. + +### Credentials + +- Steam credentials should remain in admin settings or a dedicated secure credential profile. +- Do not expose credentials in manifests shown to customers. +- Do not write raw passwords to logs. +- Prefer passing credentials through agent-managed secure files or redacted runscript generation. + +### Avoid arbitrary process/file damage + +- Install/uninstall should act only on paths from the per-item manifest. +- Remove only files/folders GSP installed or explicitly owns. +- Do not recursively delete broad paths such as `{GAME_PATH}`. +- Do not remove shared keys unless manifest ownership is clear. + +## Cross-platform Considerations + +### Linux + +- Use `steamcmd.sh`. +- Use POSIX path handling. +- Use screen-backed jobs for consistency with existing agent update behavior. +- Use `rsync` if available, otherwise safe recursive copy. +- Preserve file ownership for the game server user. + +### Cygwin/Windows + +- Use `steamcmd.exe`. +- Convert paths with `cygpath -wa` when passing to SteamCMD. +- Be careful with spaces and backslashes. +- Use Cygwin shell for shared scripts where practical. +- Avoid Linux-only tools unless bundled or checked. +- Use safe copy behavior that works under Cygwin. + +### Common risks + +- Quoting differences. +- Case-insensitive paths on Windows. +- Long paths. +- File locks while server is running. +- Different SteamCMD output. +- Different permission/ownership behavior. +- Symlink/junction behavior is not portable enough for first version. + +## DayZ-Specific Plan + +DayZ should be the first full strategy because it exercises the hard cases. + +XML: + +```xml + + 1 + steam + 221100 + 221100 + steamcmd + dayz_mod_folder + {GAME_PATH} + @{SAFE_TITLE} + -mod={MOD_LIST} + ; + + {MOD_PATH}/keys/*.bikey + {GAME_PATH}/keys + + +``` + +Panel behavior: + +- Accept Workshop URL/ID. +- Resolve title when possible. +- Store desired item row. +- Allow reorder. +- Allow enabled/disabled. +- Generate `-mod=` from enabled ordered mods. +- Prompt restart after changes. + +Agent behavior: + +- Download item. +- Install to `@ModName`. +- Copy keys. +- Write install manifest. +- Report clear failures: + - SteamCMD missing. + - Steam login required. + - Workshop item not found. + - Download path missing. + - Disk full. + - Key copy failed. + +## Generic Game Support Plan + +Each game should choose a strategy: + +- Source/Garry's Mod: `game_managed_workshop` +- Rust: likely server-managed or game-specific strategy depending on current Rust mod ecosystem support. +- DayZ: `dayz_mod_folder` +- Arma 2/3: `arma_mod_folder` +- Generic file content: `copy_to_mod_folder` or `copy_to_game_root` +- Config packs: `config_only` + +The UI can stay mostly the same while XML changes behavior behind the scenes. + +## Commercial-Quality Expected Result + +Customer experience: + +- Paste Workshop URL or ID. +- See item title if available. +- Install one or many mods. +- See progress and logs. +- Enable/disable mods without deleting them. +- Reorder mods. +- Update selected or update all. +- Uninstall cleanly. +- See restart-required prompts. +- See clear errors that explain what failed. + +Admin experience: + +- Game XML declares support. +- Install strategy is selected by game. +- Steam app IDs are configured centrally. +- Startup parameter format is configured centrally. +- Key copy behavior is configured centrally. +- Cache policy is configured centrally. +- No customer-editable helper batch files are needed. + +## Recommended Implementation Phases + +### Phase 1: Inventory/report only + +- Complete this report. +- Do not modify code. + +### Phase 2: XML schema design + +- Add `workshop_support` schema. +- Add parser helpers. +- Add validation warnings in config editor. +- Preserve old XML behavior. + +### Phase 3: DB schema + +- Add normalized Workshop metadata table. +- Add per-server Workshop item table. +- Add install job and job item tables. +- Migrate existing `server_content_workshop` rows. + +### Phase 4: Panel UI + +- Replace Phase 1 Workshop page with: + - URL/ID entry. + - Installed list. + - Enable/disable. + - Load order. + - Update/uninstall/repair. + - Live job log. + +### Phase 5: Agent job runner + +- Add first-class agent job actions. +- Return job ID immediately. +- Poll status/logs. +- Keep screen as shared backend. + +### Phase 6: SteamCMD download job + +- Implement cross-platform SteamCMD runner. +- Support anonymous and configured login. +- Redact credentials. +- Write progress/result files. + +### Phase 7: DayZ strategy + +- Download Workshop items. +- Install `@ModName` folders. +- Copy `.bikey` files. +- Generate `-mod=` startup fragment. +- Test install/update/disable/uninstall. + +### Phase 8: Startup parameter integration + +- Add structured generated startup fragments. +- Avoid duplicate raw params. +- Preserve existing game startup behavior. + +### Phase 9: Update/uninstall/reorder + +- Add update all/selected. +- Add repair. +- Add safe uninstall. +- Add load order persistence. + +### Phase 10: Cache/clone support + +- Add optional global cache. +- Add cleanup policy. +- Add clone mod list workflow. + +### Phase 11: Additional game strategies + +- Add Arma strategy. +- Add game-managed Workshop strategy for Garry's Mod/Source where appropriate. +- Add generic copy/config strategies. + +## Open Questions + +- Should the agent expose a new XML-RPC job API, or should the Panel continue to use `exec` for Phase 2? +- What should the canonical agent control path be outside customer-editable files? +- Can the current file manager/FTP layer hide `gsp_server_content` reliably if control files remain under game home? +- Which DB migration system should own the new tables? +- How should existing `server_content_workshop` rows be migrated to the proposed normalized tables? +- Where should Steam credentials be stored for Workshop items requiring login? +- Should per-server Steam credentials ever be supported, or admin-only credential profiles? +- Which games require authenticated Workshop downloads? +- How should Workshop metadata be fetched without a Steam Web API key? +- Should title resolution be required before install, or should `@` always be allowed? +- How should conflicts be handled when two Workshop items normalize to the same folder name? +- Should DayZ keys be copied recursively by default or only from configured paths? +- How should key ownership be tracked for safe uninstall? +- Should updating mods while a server is running be blocked, staged, or allowed with restart required? +- Should global cache be enabled in commercial deployments by default? +- How should disk quota account for downloaded cache and installed copies? +- Should symlinks/junctions ever be used to avoid duplicate copies? +- How should Windows long paths be handled? +- Should Workshop install logs be exposed through the existing log viewer or a dedicated Server Content job log page? + +## Summary Recommendation + +Use the current Phase 1 Server Content work as a starting point, but do not keep extending it as a synchronous script runner. + +The professional path is: + +- Add XML-declared Workshop/content capabilities. +- Store desired Workshop state in the Panel database. +- Add pollable agent jobs for install/update/remove. +- Use SteamCMD on the agent. +- Implement game-specific strategies, starting with DayZ. +- Generate startup parameters from structured enabled/load-ordered rows. +- Treat LGSL/GameQ and game query systems as unrelated to content installation. +- Keep customers away from raw commands, helper scripts, and managed control files. + diff --git a/docs/agents/LINUX_AGENT.md b/docs/agents/LINUX_AGENT.md new file mode 100644 index 00000000..2aa3a4e1 --- /dev/null +++ b/docs/agents/LINUX_AGENT.md @@ -0,0 +1,109 @@ +# Linux Agent + +## Role + +`Agent_Linux/ogp_agent.pl` is the Linux execution agent for GSP. It is responsible for: + +- starting and stopping game servers +- managing `screen` sessions +- reading logs +- running update/install tasks +- handling scheduler jobs +- performing query checks and status checks + +## Important Files + +- `Agent_Linux/ogp_agent.pl` +- `Agent_Linux/startups/` +- `Agent_Linux/includes/` +- `Agent_Linux/php-query/` +- `Agent_Linux/Cfg/` +- `Agent_Linux/Schedule/` +- `Agent_Linux/steamcmd/` +- `Agent_Linux/systemd/` + +## Startup Logic + +The Linux agent creates a managed `screen` session per server. + +Key flow in `ogp_agent.pl`: + +- `universal_start_without_decrypt` +- `create_screen_cmd` +- `create_screen_cmd_loop` +- `replace_OGP_Env_Vars` + +The agent builds the startup command from: + +- the game XML template +- server parameters +- mod data +- control password +- path variables + +The session name follows the OGP naming convention, for example: + +```text +OGP_HOME_000000123 +``` + +## Status Logic + +Relevant functions: + +- `is_screen_running_without_decrypt` +- `get_screen_pid_without_decrypt` +- `server_status_without_decrypt` +- `verify_server_stopped_without_decrypt` + +The status implementation should check: + +- screen/session existence +- PID or child process information when available +- whether the game port is listening +- whether query metadata can be fetched + +Marker files such as `SERVER_STOPPED` should not be treated as the final source of truth. + +## Logging + +Relevant function: + +- `get_log` + +The agent reads screen logs and may also copy a local log file into the game home. Logs should be treated as runtime output, not as a state store. + +## Scheduler + +Linux scheduler functions live in `ogp_agent.pl`: + +- `scheduler_dispatcher` +- `scheduler_server_action` +- `scheduler_log_events` +- `scheduler_add_task` +- `scheduler_del_task` +- `scheduler_edit_task` +- `scheduler_read_tasks` +- `scheduler_stop` +- `scheduler_list_tasks` + +The scheduler is agent-driven. Panel pages create or edit jobs, but the agent executes them. + +## Configuration Files + +Useful configuration and runtime areas: + +- `Agent_Linux/Cfg/` +- `Agent_Linux/startups/` +- `Agent_Linux/steamcmd/` +- `Agent_Linux/systemd/` + +The agent also maintains screen logs and helper scripts inside its runtime area. + +## Linux-Specific Notes + +- The Linux agent uses `screen` and `sudo_exec_without_decrypt`. +- It can resolve server ownership and screen users via helper functions such as `find_user_by_screen_id`. +- It must remain portable across distro variants, so avoid assuming one exact init system or one exact binary path. +- For Windows-targeted games running under Linux, Wine-related path conversion appears in startup path handling. + diff --git a/docs/agents/WINDOWS_AGENT.md b/docs/agents/WINDOWS_AGENT.md new file mode 100644 index 00000000..1d0d2506 --- /dev/null +++ b/docs/agents/WINDOWS_AGENT.md @@ -0,0 +1,94 @@ +# Windows Agent + +## Role + +`Agent-Windows/ogp_agent.pl` is the Windows/Cygwin execution agent for GSP. It mirrors the Linux agent as closely as practical, while using Windows-compatible paths, processes, and wrappers. + +## Important Files + +- `Agent-Windows/ogp_agent.pl` +- `Agent-Windows/php-query/` +- `Agent-Windows/ArmaBE/` +- `Agent-Windows/Cfg/` +- `Agent-Windows/Install/` +- `Agent-Windows/ServerFiles/` +- `Agent-Windows/Schedule/` + +## Cygwin Requirements + +The Windows agent assumes a Cygwin-style environment that can provide: + +- `screen` +- Perl +- shell utilities such as `ps`, `grep`, `cut`, `awk`, `sed` +- `cygpath` +- a usable `bash` + +The goal is to keep the Windows agent behavior close to the Linux agent so the Panel does not need separate semantics for basic lifecycle operations. + +## Startup Logic + +Relevant functions: + +- `universal_start_without_decrypt` +- `create_screen_cmd` +- `create_screen_cmd_loop` +- `replace_OGP_Env_Vars` + +The Windows agent also uses `screen` sessions for managed server execution. Depending on the game and binary type, it may wrap commands in `cmd /Q /C start` or run a batch file wrapper. + +The session naming scheme also follows the OGP convention: + +```text +OGP_HOME_000000123 +``` + +## Status Logic + +Relevant functions: + +- `is_screen_running_without_decrypt` +- `get_screen_pid_without_decrypt` +- `server_status_without_decrypt` +- `verify_server_stopped_without_decrypt` + +The status model should check: + +- screen/session existence +- process/PID information when available +- game port listening +- optional query metadata + +The old `SERVER_STOPPED` file should not be the source of truth. + +## Logging + +Relevant function: + +- `get_log` + +Windows/Cygwin logs come from screen logs and/or local copies. Log retrieval should remain compatible with the Panel's AJAX log view. + +## Scheduler + +Relevant functions: + +- `scheduler_dispatcher` +- `scheduler_server_action` +- `scheduler_log_events` +- `scheduler_add_task` +- `scheduler_del_task` +- `scheduler_edit_task` +- `scheduler_read_tasks` +- `scheduler_stop` +- `scheduler_list_tasks` + +The Windows scheduler implementation should remain aligned with the Linux scheduler implementation so the Panel can treat both the same way. + +## Windows-Specific Notes + +- Path conversion between Cygwin and native Windows paths matters during startup. +- Batch wrappers are often needed for Windows executables. +- Process cleanup must avoid killing unrelated processes that happen to share an executable name. +- The agent should continue to use `screen` where it already does so, to stay aligned with Linux behavior. + diff --git a/docs/architecture/PANEL_AGENT_FLOW.md b/docs/architecture/PANEL_AGENT_FLOW.md new file mode 100644 index 00000000..b61e8d56 --- /dev/null +++ b/docs/architecture/PANEL_AGENT_FLOW.md @@ -0,0 +1,103 @@ +# Panel Agent Flow + +## Overview + +The Panel does not directly run servers. It prepares the request, sends it to the agent, and interprets the response. + +```text +User action + -> Panel module page + -> OGPRemoteLibrary in lib_remote.php + -> XML-RPC request to agent /RPC2 + -> ogp_agent.pl method + -> local screen/process/port work + -> return status or payload + -> Panel renders result +``` + +## Request Path + +Important Panel files: + +- `Panel/includes/lib_remote.php` +- `Panel/modules/gamemanager/start_server.php` +- `Panel/modules/gamemanager/stop_server.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/gamemanager/server_monitor.php` +- `Panel/modules/gamemanager/view_server_log.php` +- `Panel/modules/gamemanager/get_server_log.php` +- `Panel/modules/gamemanager/home_handling_functions.php` + +### Start + +1. Panel loads the selected server home and game XML. +2. Panel composes startup parameters from stored values and XML templates. +3. Panel calls `universal_start` on the agent. +4. Agent creates the `screen` session and starts the server command. +5. Panel polls `remote_server_status`. +6. When the agent reports that the process/session exists and the game port listens, the server is treated as ONLINE. + +### Stop + +1. Panel calls `remote_stop_server`. +2. Agent sends the configured graceful stop command if one exists. +3. Agent watches the session/process and required port. +4. If the session/process does not exit in time, the agent escalates to screen quit or process kill. +5. Panel treats the server as stopped only when the agent confirms it is actually gone. + +### Restart + +Restart should be: + +```text +stop + -> wait + -> start +``` + +The wait period should be explicit and visible in logs. Restart should not depend on marker files or query success. + +### Status + +1. Panel calls `remote_server_status`. +2. Agent checks: + - managed screen/session + - PID or process tree when available + - required game port + - optional query/RCON ports +3. Agent returns a structured status object. +4. Panel maps the result to the UI state. + +### Logs + +1. Panel calls `get_log`. +2. Agent returns the latest screen or console log data. +3. The log view updates via AJAX instead of full-page refresh. +4. The UI should preserve scroll position and only auto-scroll when the user is already at the bottom. + +## Data Returned by Status + +Recommended status fields: + +- `status` +- `ready` +- `process_running` +- `session_running` +- `game_port_listening` +- `query_port_listening` +- `rcon_port_listening` +- `pid` +- `session_name` +- `ip` +- `port` +- `query_port` +- `last_error` +- optional query metadata + +## Practical Notes + +- The Panel should not mark a server failed only because query metadata was unavailable. +- The agent should be the source of truth. +- Marker files may exist, but they should be treated as hints only. +- The same high-level flow should work for Linux and Windows/Cygwin. + diff --git a/docs/architecture/REPOSITORY_OVERVIEW.md b/docs/architecture/REPOSITORY_OVERVIEW.md new file mode 100644 index 00000000..94e9c345 --- /dev/null +++ b/docs/architecture/REPOSITORY_OVERVIEW.md @@ -0,0 +1,159 @@ +# Repository Overview + +## Purpose + +This repository contains the GSP game server hosting platform: + +- `Panel` - the web control panel and customer/admin UI. +- `Agent_Linux` - the Linux agent that starts, stops, monitors, updates, and logs game servers. +- `Agent-Windows` - the Windows/Cygwin agent that mirrors the Linux agent as closely as possible. +- `Website` - the public marketing, customer, and commerce site. + +## Top-Level Layout + +```text +/ + Agent_Linux/ + Agent-Windows/ + Panel/ + Website/ + docs/ +``` + +## Major Components + +### Panel + +The Panel is the orchestration layer. It: + +- loads module pages from `Panel/modules/*` +- talks to agents through `Panel/includes/lib_remote.php` +- stores panel-side state in the database +- renders server lifecycle, file, backup, scheduler, Workshop, support, and billing pages + +Important Panel files: + +- `Panel/includes/lib_remote.php` +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/server_monitor.php` +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/addonsmanager/module.php` +- `Panel/modules/cron/module.php` +- `Panel/modules/user_games/module.php` + +### Agents + +The agents are the execution layer. They: + +- launch game servers inside `screen` +- stop and restart servers +- read logs +- run updates and install jobs +- execute scheduler jobs +- report status back to the Panel + +Important agent files: + +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` +- `Agent_Linux/startups/` +- `Agent-Windows/ServerFiles/` +- `Agent_Linux/php-query/` +- `Agent-Windows/php-query/` + +### Website + +The Website is separate from the server runtime path. It is used for: + +- public product pages +- documentation links +- customer onboarding +- billing and commerce surfaces + +## Panel <-> Agent Communication + +The Panel uses XML-RPC over HTTP to call methods exposed by `ogp_agent.pl`. + +The remote wrapper lives in: + +- `Panel/includes/lib_remote.php` + +The most important calls are: + +- `universal_start` +- `remote_stop_server` +- `remote_restart_server` +- `remote_server_status` +- `is_screen_running` +- `get_log` +- `remote_query` +- scheduler methods + +The agents decode the request, execute the action locally, and return a status code or payload. + +## Database Usage + +The Panel database stores: + +- server homes and assigned ports +- game definitions and mod mappings +- user permissions and subusers +- scheduler definitions and related data +- content and Workshop metadata +- tickets/support records +- billing and provisioning records + +Key tables discovered in current code: + +- `server_homes` +- `home_ip_ports` +- `game_mods` +- `status_cache` +- `config_homes` +- `config_mods` +- `addons` +- `server_content_workshop` +- `server_content_manifest` +- `server_content_install_history` +- `tickets` +- `ticket_messages` +- `ticket_attachments` +- `ticket_settings` +- billing-related tables in `Panel/modules/billing` + +The agents also keep runtime files such as screen logs, update logs, scheduler state, and helper manifests, but those should be treated as runtime state rather than the source of truth. + +## Startup Flow + +1. User clicks start in the Panel. +2. Panel builds the startup command from the game XML and stored server parameters. +3. Panel sends `universal_start` to the agent. +4. Agent creates a `screen` session and launches the server command. +5. Panel polls status and logs. +6. If the managed session exists and the required port listens, the server is considered online. + +## Status Reporting Flow + +The desired status flow is: + +```text +Panel asks agent for status + -> agent checks managed session/process + -> agent checks required listening port + -> agent optionally performs query metadata lookup + -> agent returns structured status + -> Panel renders ONLINE / STARTING / STOPPING / UNRESPONSIVE / OFFLINE / UNKNOWN +``` + +Query results are metadata only. They are not the source of truth for online/offline state. + +## What To Read First + +For future investigations, start with: + +1. `docs/development/CODEX_GUIDE.md` +2. `docs/architecture/PANEL_AGENT_FLOW.md` +3. `docs/modules/GAMEMANAGER.md` +4. `docs/features/STATUS_SYSTEM.md` +5. `docs/features/XML_SYSTEM.md` + diff --git a/docs/development/CODEX_GUIDE.md b/docs/development/CODEX_GUIDE.md new file mode 100644 index 00000000..dd6189ea --- /dev/null +++ b/docs/development/CODEX_GUIDE.md @@ -0,0 +1,138 @@ +# Codex Guide + +This file is the first stop for future Codex sessions working in this repository. + +## Repository Layout + +```text +/ + Agent_Linux/ + Agent-Windows/ + Panel/ + Website/ + docs/ +``` + +## What To Read First + +1. `docs/architecture/REPOSITORY_OVERVIEW.md` +2. `docs/architecture/PANEL_AGENT_FLOW.md` +3. `docs/modules/MODULE_INDEX.md` +4. `docs/modules/GAMEMANAGER.md` +5. `docs/features/STATUS_SYSTEM.md` +6. `docs/features/XML_SYSTEM.md` +7. `docs/modules/SCHEDULER.md` +8. `docs/modules/SERVER_CONTENT_MANAGER.md` + +## Important Files By Topic + +### Startup Logic + +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/start_server.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/server_config_parser.php` +- `Panel/includes/lib_remote.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +### Status Logic + +- `Panel/includes/lib_remote.php` +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/server_monitor.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +### Scheduler Logic + +- `Panel/modules/cron/module.php` +- `Panel/modules/cron/cron.php` +- `Panel/modules/cron/shared_cron_functions.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +### Workshop / Server Content Logic + +- `Panel/modules/addonsmanager/module.php` +- `Panel/modules/addonsmanager/addons_manager.php` +- `Panel/modules/addonsmanager/user_addons.php` +- `Panel/modules/addonsmanager/workshop_content.php` +- `Panel/modules/addonsmanager/workshop_action.php` +- `Panel/modules/steam_workshop/module.php` + +### XML Definitions + +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/xml_config_creator.php` +- `Panel/modules/config_games/set_params.php` +- `Panel/modules/config_games/cli-params.php` +- `Panel/modules/config_games/config_servers.php` + +### Agent Communication + +- `Panel/includes/lib_remote.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +## Common Development Workflows + +### Debug a start/stop/restart issue + +1. Read `docs/modules/GAMEMANAGER.md`. +2. Check `Panel/includes/lib_remote.php`. +3. Check `Panel/modules/gamemanager/home_handling_functions.php`. +4. Check the matching `ogp_agent.pl`. +5. Compare session/process/port logic in both agents. + +### Debug a status issue + +1. Read `docs/features/STATUS_SYSTEM.md`. +2. Check `remote_server_status` in `Panel/includes/lib_remote.php`. +3. Check `server_status_without_decrypt` in both agents. +4. Check game XML query definitions in `config_games`. + +### Debug scheduler behavior + +1. Read `docs/modules/SCHEDULER.md`. +2. Check `Panel/modules/cron/cron.php`. +3. Check scheduler subroutines in both agents. +4. Verify whether the action is customer-safe or admin-only. + +### Debug Workshop or add-on behavior + +1. Read `docs/modules/SERVER_CONTENT_MANAGER.md`. +2. Check `Panel/modules/addonsmanager/module.php`. +3. Check the user/admin content pages. +4. Check whether the action should be treated as install, update, or uninstall. + +## Things Already Investigated + +The repository has already been mapped in these areas: + +- module inventory +- panel-agent remote library +- Linux and Windows agent `screen` use +- status model direction +- game XML startup and query variables +- current Server Content Manager structure +- current scheduler structure +- module-level roles and dependency patterns + +## Things Intentionally Not Yet Implemented + +This documentation-only pass does not implement: + +- lifecycle code changes +- status model code changes +- scheduler redesign +- Workshop/content redesign +- backup system replacement +- file manager or FTP rewrites +- billing/provisioning changes + +## Practical Rule for Future Sessions + +Before scanning code broadly, read the docs layer first. Only open source files when the documentation does not already answer the question. + diff --git a/docs/features/STATUS_SYSTEM.md b/docs/features/STATUS_SYSTEM.md new file mode 100644 index 00000000..7bdab95b --- /dev/null +++ b/docs/features/STATUS_SYSTEM.md @@ -0,0 +1,54 @@ +# Status System + +## Current Goal + +The status system should tell the truth about a server's state without depending on stale marker files or query success alone. + +## Current Direction + +The current codebase is moving toward agent-owned structured status. + +Important files: + +- `Panel/includes/lib_remote.php` +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/server_monitor.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +## Recommended State Model + +| State | Meaning | +|---|---| +| `OFFLINE` | No managed session/process and no required port listening. | +| `STARTING` | Managed session/process exists but the required port is not ready yet. | +| `ONLINE` | Managed session/process exists and the required port is listening. | +| `STOPPING` | Stop is in progress and the session/process still exists. | +| `UNRESPONSIVE` | The server did not become ready in time or stop did not complete cleanly. | +| `UNKNOWN` | The agent cannot be reached or cannot determine state. | + +## What Should Be Checked + +The agent should check, in this order: + +1. managed session or screen name +2. process or PID tree when available +3. required game port listening +4. optional query/RCON port listening +5. optional query metadata + +## Known Problems To Remember + +- LGSL/GameQ may fail for supported games, blocked ports, or slow startups. +- Marker files can become stale after crashes or power loss. +- A game can be online even if query metadata is temporarily unavailable. +- Some games need long startup windows, so timeouts must be configurable per game. + +## Planned Improvement Shape + +- make the agent the source of truth +- preserve optional query metadata +- use state hints for start/stop transitions +- expose clear messages for `STARTING` and `UNRESPONSIVE` +- add precise log excerpts when startup fails + diff --git a/docs/features/WORKSHOP_SYSTEM.md b/docs/features/WORKSHOP_SYSTEM.md new file mode 100644 index 00000000..c02ed05a --- /dev/null +++ b/docs/features/WORKSHOP_SYSTEM.md @@ -0,0 +1,53 @@ +# Workshop System + +## Current State + +The current Workshop/content work is split across two module lines: + +- `Panel/modules/steam_workshop` - deprecated compatibility layer +- `Panel/modules/addonsmanager` - the active Server Content Manager path + +Important files: + +- `Panel/modules/addonsmanager/module.php` +- `Panel/modules/addonsmanager/user_addons.php` +- `Panel/modules/addonsmanager/addons_manager.php` +- `Panel/modules/addonsmanager/workshop_content.php` +- `Panel/modules/addonsmanager/workshop_action.php` +- `Panel/modules/steam_workshop/module.php` +- `Panel/modules/steam_workshop/agent_update_workshop.php` + +## What Exists Today + +The current direction already supports: + +- content records in the Panel database +- Workshop item IDs +- installation metadata +- install history tables +- game compatibility fields +- launch parameter additions +- post-install behavior fields + +## Main Limitations + +- Workshop metadata is still incomplete. +- load order is not yet a full first-class UX concept. +- update/uninstall/enable/disable flows need a cleaner product model. +- DayZ/Arma-specific folder and key-copy behavior needs a stronger canonical path. +- caching and cleanup policy need product-level design, not just ad hoc scripts. + +## Recommended Mental Model + +Use `addonsmanager` as the main future home for: + +- mods +- add-ons +- Workshop items +- scripts +- config packs +- server content manifests +- install history + +Treat `steam_workshop` as a legacy bridge for migration only. + diff --git a/docs/features/XML_SYSTEM.md b/docs/features/XML_SYSTEM.md new file mode 100644 index 00000000..af6a82db --- /dev/null +++ b/docs/features/XML_SYSTEM.md @@ -0,0 +1,105 @@ +# XML Game Configuration System + +## Purpose + +The XML game configuration system describes how a game server should be started, queried, and customized. + +Primary files: + +- `Panel/modules/config_games/schema_server_config.xml` +- `Panel/modules/config_games/server_config_parser.php` +- `Panel/modules/config_games/xml_config_creator.php` +- `Panel/modules/config_games/config_servers.php` +- `Panel/modules/config_games/cli-params.php` +- `Panel/modules/config_games/set_params.php` + +## What XML Controls + +The schema supports: + +- game and installer names +- startup command templates +- CLI parameter substitution +- reserved ports +- query port calculation +- control protocol selection +- mod definitions +- custom fields +- server parameter groups +- text replacement helpers + +## Important Variables + +The schema and startup builder can work with variables such as: + +- `GAME_TYPE` +- `HOSTNAME` +- `IP` +- `MAP` +- `PID_FILE` +- `PLAYERS` +- `PORT` +- `QUERY_PORT` +- `BASE_PATH` +- `HOME_PATH` +- `SAVE_PATH` +- `OUTPUT_PATH` +- `USER_PATH` +- `CONTROL_PASSWORD` + +## Startup Parameters + +The Panel builds startup parameters from the XML template and the stored server configuration. + +Key concepts: + +- `cli_template` +- `cli_params` +- `reserve_ports` +- `server_params` +- `custom_fields` +- `clean_server_param_value` + +The XML file defines: + +- which parameters exist +- how they are quoted or spaced +- whether the parameter is editable by the customer +- what defaults should be used + +## Query Definitions + +The XML schema supports query-related concepts such as: + +- `gameq` +- `lgsl` +- `teamspeak3` +- query port offset calculations +- control protocol selection + +These values are used by `gamemanager` and the agent status logic to calculate query metadata, not to decide online/offline by themselves. + +## Installation and File Editing + +XML definitions also feed: + +- config file shortcuts +- install-time behavior +- docs links +- reserved ports +- mod or content behavior + +## Recommended Mental Model + +Think of the XML system as the capability definition layer: + +```text +game XML + -> startup template + -> parameter rules + -> query rules + -> content/mod hooks + -> docs links + -> scheduler and status hints +``` + diff --git a/docs/modules/GAMEMANAGER.md b/docs/modules/GAMEMANAGER.md new file mode 100644 index 00000000..0f3065b8 --- /dev/null +++ b/docs/modules/GAMEMANAGER.md @@ -0,0 +1,82 @@ +# GameManager + +## Role + +`Panel/modules/gamemanager` is the core customer server control module. It owns: + +- start +- stop +- restart +- logs +- server monitor +- update flow hooks +- RCON integration +- status display + +## Important Files + +- `Panel/modules/gamemanager/module.php` +- `Panel/modules/gamemanager/home_handling_functions.php` +- `Panel/modules/gamemanager/server_monitor.php` +- `Panel/modules/gamemanager/start_server.php` +- `Panel/modules/gamemanager/stop_server.php` +- `Panel/modules/gamemanager/restart_server.php` +- `Panel/modules/gamemanager/log.php` +- `Panel/modules/gamemanager/view_server_log.php` +- `Panel/modules/gamemanager/get_server_log.php` +- `Panel/modules/gamemanager/update_server.php` + +## Start / Stop / Restart + +The module calls the agent through `Panel/includes/lib_remote.php`. + +Current shape: + +- start -> `universal_start` +- stop -> `remote_stop_server` +- restart -> `remote_restart_server` + +The right behavior is: + +- start shows `STARTING` as soon as the managed session exists +- online status requires the process/session plus the game port +- restart should be stop, wait, start +- stop should not be treated as complete until the session/process is actually gone + +## Status Reporting + +The current flow now points toward agent-truth status reporting via: + +- `remote_server_status` +- `get_agent_server_status` in `home_handling_functions.php` +- `server_status_without_decrypt` in both agents + +Useful state labels: + +- `OFFLINE` +- `STARTING` +- `ONLINE` +- `STOPPING` +- `UNRESPONSIVE` +- `UNKNOWN` + +Query checks should remain optional metadata only. + +## Log Viewer + +Relevant files: + +- `Panel/modules/gamemanager/log.php` +- `Panel/modules/gamemanager/view_server_log.php` +- `Panel/modules/gamemanager/get_server_log.php` + +The log view should be treated as live, AJAX-updated output rather than a full page reload workflow. + +## What This Module Depends On + +- `config_games` for startup parameters and protocol definitions +- `lib_remote.php` for agent calls +- `user_games` for server home records +- `rcon` for command support where available +- `addonsmanager` for content/mod interactions + diff --git a/docs/modules/MODULE_INDEX.md b/docs/modules/MODULE_INDEX.md new file mode 100644 index 00000000..3d174cf6 --- /dev/null +++ b/docs/modules/MODULE_INDEX.md @@ -0,0 +1,68 @@ +# Module Index + +This is the current Panel module inventory. It is intentionally concise so future Codex sessions can decide which module to inspect in code. + +| Module | Purpose | Current State | Dependencies | Notes | +|---|---|---|---|---| +| `TS3Admin` | Teamspeak 3 admin interface | Required, legacy niche | TS3 admin files | Keep if TS3 hosting is sold. | +| `addonsmanager` | Server Content Manager | Required, actively evolving | DB tables, game XML, agent install scripts | Best current home for mods, add-ons, Workshop, and content installs. | +| `administration` | Admin utilities | Required | Core admin pages | Includes logger/watch tools. | +| `backup-restore` | Backup/restore UI | Optional, broken/testing | Hard-coded backup host/path logic | Hide until replaced. | +| `billing` | Billing, provisioning, commerce | Optional, large custom module | Payment gateways, invoices, shop/provisioning docs | Important for commercial hosting. | +| `circular` | Notification/circular messages | Optional | Panel UI | Good candidate for maintenance and announcement notices. | +| `config_games` | Game XML definitions and CLI builder | Required | XML schema/parser | Critical for startup templates, queries, custom fields, and game capabilities. | +| `cron` | Scheduler / CRON | Required | Agent scheduler methods, Panel action selection | Needs safe action registry and task history. | +| `dashboard` | Main landing dashboard | Required | Panel auth and server summaries | Should surface status, support, billing, and alerts. | +| `dsi` | Dynamic Server Image | Optional | Game imagery and cached assets | Useful for server cards and branding. | +| `editconfigfiles` | Config file shortcuts | Optional | Game config metadata | Good for surfacing common editable files. | +| `faq` | FAQ/help | Required | Site docs/content | Should link to game docs and common workflows. | +| `fast_download` | FastDL support | Required | Source/GoldSrc-style web distribution | Still useful for older Source engine communities. | +| `ftp` | FTP admin | Required | File transfer service, access rights | Needs security review but remains important. | +| `gamemanager` | Server monitor, lifecycle, logs, RCON | Required | Agent RPC, game XML, query libraries | Core customer workflow module. | +| `lgsl_with_img_mod` | LGSL server status images | Optional legacy | Query/image cache data | Secondary to agent truth. | +| `litefm` | In-panel file manager | Required | File system access rights | Should be the preferred in-panel file tool. | +| `lostpwd` | Password recovery | Required | Auth/account flow | Basic account support. | +| `modulemanager` | Module installation/configuration | Required | Module metadata | Admin maintenance tool. | +| `mysql` | MySQL hosting/admin | Required | MySQL service setup | Good future product tie-in for databases. | +| `news` | Legacy news/announcements | Optional legacy | Old CMS-like data | Consider hiding unless modernized. | +| `rcon` | RCON admin tool | Required | Server protocol support | Useful for commands, warnings, and scheduler integration. | +| `register` | Account registration | Required | Auth flow | Basic customer onboarding. | +| `server` | Server manager | Required | Agent/node management | Admin-facing node controls. | +| `settings` | Global settings | Required | Auth, site config | Admin configuration area. | +| `status` | Status page | Optional alpha | Status data | Not ready for customer-facing critical use. | +| `steam_workshop` | Legacy Workshop module | Optional deprecated | Workshop DB helpers | Hidden/deprecated in favor of `addonsmanager`. | +| `subusers` | Subuser permissions | Required | Authorization model | Important for commercial teams and communities. | +| `support` | Support landing page | Required | Ticketing/docs | Better as an entry point than the full ticket workflow. | +| `teamspeak3` | Teamspeak 3 web interface | Required if sold | TS3 service | Hide if not part of the product offering. | +| `tickets` | Support ticket system | Optional but useful | DB tables, attachments, notifications | Stronger support workflow than `support` alone. | +| `tshock` | Terraria/TShock utilities | Optional niche | Terraria/TShock game support | Expose only for supported games. | +| `update` | Panel updates | Required admin tool | Patch/update system | Admin-only maintenance. | +| `user_admin` | User management | Required | Auth/admin roles | Important for staff administration. | +| `user_games` | Server provisioning and assignment | Required | Game homes, ports, billing integration | Core provisioning path. | +| `util` | Utility tools | Required | Misc tools | Keep useful tools, hide legacy helpers that are not maintained. | + +## Dependency Notes + +Common dependencies across many modules: + +- `includes/lib_remote.php` +- auth/session and role checks +- `config_games` XML parsing +- DB access helpers +- server home and IP/port records + +## High-Value Modules + +The modules most likely to matter in future investigations are: + +1. `gamemanager` +2. `config_games` +3. `user_games` +4. `addonsmanager` +5. `cron` +6. `litefm` +7. `ftp` +8. `billing` +9. `tickets` +10. `subusers` + diff --git a/docs/modules/SCHEDULER.md b/docs/modules/SCHEDULER.md new file mode 100644 index 00000000..1c2d23d2 --- /dev/null +++ b/docs/modules/SCHEDULER.md @@ -0,0 +1,96 @@ +# Scheduler + +## Current Implementation + +The scheduler lives in the `cron` module on the Panel and in scheduler methods inside the agents. + +Important files: + +- `Panel/modules/cron/module.php` +- `Panel/modules/cron/cron.php` +- `Panel/modules/cron/shared_cron_functions.php` +- `Agent_Linux/ogp_agent.pl` +- `Agent-Windows/ogp_agent.pl` + +## How It Works Today + +The Panel builds cron-like jobs and sends them to the agent using RPC methods such as: + +- `scheduler_add_task` +- `scheduler_edit_task` +- `scheduler_del_task` +- `scheduler_list_tasks` +- `scheduler_read_tasks` + +The agent stores and executes the tasks locally. + +## Current Task Model + +Current jobs are built as cron expressions plus a command string. + +The command can be: + +- a safe predefined server action +- an update-related action +- a server content action +- a raw admin command + +## Scheduled Actions Observed + +From the current codebase and related Server Content work, the action set includes: + +- restart server +- stop server +- start server +- Steam auto update +- server content update checks +- server content install/update actions +- validate files +- backup before update +- notify-only update flow + +## Problems With the Current Design + +- The task model is not strongly typed. +- Customer-safe and admin-only actions are too easy to blur together. +- Task output and run history are not user-friendly enough. +- The agent is the executor, but the Panel needs a better task record and result view. +- Overlap/conflict rules are not explicit. +- Missed-task behavior after reboot should be documented and normalized. + +## Recommended Next-Step Shape + +Use typed actions instead of raw command strings for customer-facing scheduler tasks. + +Suggested fields: + +- action key +- display name +- role +- arguments +- timeout +- retry behavior +- conflict rules +- log policy + +## Agent Interaction Pattern + +```text +Panel schedules task + -> Panel stores intent and UI data + -> Panel sends job to agent + -> agent writes/loads scheduler state + -> agent executes action at the right time + -> agent logs result + -> Panel reads back task state +``` + +## What To Inspect in Code + +If scheduler behavior needs deeper investigation, start with: + +- `Panel/modules/cron/cron.php` +- `Panel/modules/cron/shared_cron_functions.php` +- `Agent_Linux/ogp_agent.pl` scheduler subroutines +- `Agent-Windows/ogp_agent.pl` scheduler subroutines + diff --git a/docs/modules/SERVER_CONTENT_MANAGER.md b/docs/modules/SERVER_CONTENT_MANAGER.md new file mode 100644 index 00000000..2f5722e0 --- /dev/null +++ b/docs/modules/SERVER_CONTENT_MANAGER.md @@ -0,0 +1,70 @@ +# Server Content Manager + +## Current State + +`Panel/modules/addonsmanager` is the current home of GSP's Server Content / Add-ons / Workshop work. + +The module title has already been moved toward `Server Content Manager`, but the schema and some folder names remain backward-compatible. + +Important files: + +- `Panel/modules/addonsmanager/module.php` +- `Panel/modules/addonsmanager/addons_manager.php` +- `Panel/modules/addonsmanager/user_addons.php` +- `Panel/modules/addonsmanager/workshop_content.php` +- `Panel/modules/addonsmanager/workshop_action.php` +- `Panel/modules/addonsmanager/server_content_helpers.php` +- `Panel/modules/addonsmanager/server_content_categories.php` + +## Database Tables + +Known tables used by the module: + +- `addons` +- `server_content_workshop` +- `server_content_manifest` +- `server_content_install_history` + +## What It Already Does + +The module can already represent several content types, including: + +- downloads/extracted packages +- post-script driven installs +- workshop-oriented items +- config packs +- future profile-type content + +For Workshop items, the current flow lets users enter IDs and routes the install through module pages and agent-side scripts. + +## Current Limitations + +- Workshop and content metadata is still partial. +- Load order and enable/disable behavior need a cleaner first-class model. +- Async install job progress should be more visible. +- Install strategies are still being broadened and need consistent game-specific rules. +- DayZ/Arma style key-copy and startup-param behavior needs a stronger canonical implementation. +- Cache and cleanup policy need a clearer product design. + +## Where To Start Reading + +1. `Panel/modules/addonsmanager/module.php` +2. `Panel/modules/addonsmanager/addons_manager.php` +3. `Panel/modules/addonsmanager/user_addons.php` +4. `Panel/modules/addonsmanager/workshop_content.php` +5. `Panel/modules/addonsmanager/workshop_action.php` + +## Important Concept + +This module is the right place for: + +- mods +- add-ons +- Workshop content +- config packs +- script-driven installs +- server content manifests +- install history + +The old `steam_workshop` module should be treated as a deprecated compatibility layer, not the main future path. +