update dix

This commit is contained in:
Frank Harris 2026-06-06 14:21:58 -05:00
parent e921a49d5b
commit 11691a5876
11 changed files with 1108 additions and 54 deletions

View file

@ -340,6 +340,7 @@ my $d = Frontier::Daemon::OGP::Forking->new(
fastdl_get_info => \&fastdl_get_info,
fastdl_create_config => \&fastdl_create_config,
agent_restart => \&agent_restart,
component_update => \&component_update,
scheduler_add_task => \&scheduler_add_task,
scheduler_del_task => \&scheduler_del_task,
scheduler_list_tasks => \&scheduler_list_tasks,
@ -3498,6 +3499,162 @@ sub fastdl_create_config
return 1;
}
sub component_update_shell_quote
{
my ($value) = @_;
$value = '' unless defined $value;
$value =~ s/'/'"'"'/g;
return "'" . $value . "'";
}
sub component_update_response
{
my (%data) = @_;
my %encoded = ();
foreach my $key (keys %data)
{
$encoded{$key} = encode_base64(defined $data{$key} ? $data{$key} : '', '');
}
return \%encoded;
}
sub component_update_parse_payload
{
my ($payload) = @_;
my %data = ();
foreach my $line (split(/\r?\n/, $payload || ''))
{
next if $line !~ /^([A-Za-z0-9_]+)=(.*)$/;
$data{$1} = decode_base64($2);
}
return %data;
}
sub component_update
{
return component_update_response(success => 0, status => 'bad_encryption', message => 'Bad Encryption Key')
unless(decrypt_param(pop(@_)) eq "Encryption checking OK");
my ($payload) = decrypt_params(@_);
my %cfg = component_update_parse_payload($payload);
my $component = $cfg{component} || 'windows_agent';
if ($component ne 'linux_agent' && $component ne 'windows_agent')
{
return component_update_response(success => 0, status => 'invalid_component', message => 'Invalid component.');
}
my $repo_url = $cfg{repo_url} || '';
my $branch = $cfg{branch} || '';
my $source_path = $cfg{source_path} || '';
my $install_path = $cfg{install_path} || AGENT_RUN_DIR;
my $git_path = $cfg{git_path} || 'git';
my $backup_path = $cfg{backup_path} || Path::Class::Dir->new(AGENT_RUN_DIR, 'component_backups')->stringify;
my $post_update_command = $cfg{post_update_command} || '';
if ($repo_url !~ m{^(https?://|ssh://|git@|/)} || $repo_url =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_repo', message => 'Invalid repository URL or path.');
}
if ($branch !~ /^[A-Za-z0-9._\/-]{1,128}$/)
{
return component_update_response(success => 0, status => 'invalid_branch', message => 'Invalid branch.');
}
if ($source_path eq '' || $source_path =~ /\.\./ || $source_path =~ /[\r\n\0]/ || $source_path !~ /^[A-Za-z0-9._\/-]+$/)
{
return component_update_response(success => 0, status => 'invalid_source_path', message => 'Invalid source path.');
}
if ($install_path !~ m{^/} || $install_path =~ /\.\./ || $install_path =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_install_path', message => 'Invalid install path.');
}
if ($post_update_command =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_post_update', message => 'Post-update command must be a single line.');
}
my $ts = time();
my $script_path = Path::Class::File->new(AGENT_RUN_DIR, "gsp_component_update_$ts.sh")->stringify;
my $log_path = Path::Class::File->new(AGENT_RUN_DIR, 'gsp_component_update.log')->stringify;
my $screenrc = SCREENRC_FILE;
my $repo_q = component_update_shell_quote($repo_url);
my $branch_q = component_update_shell_quote($branch);
my $source_q = component_update_shell_quote($source_path);
my $install_q = component_update_shell_quote($install_path);
my $git_q = component_update_shell_quote($git_path);
my $backup_q = component_update_shell_quote($backup_path);
my $post_q = component_update_shell_quote($post_update_command);
my $log_q = component_update_shell_quote($log_path);
my $screenrc_q = component_update_shell_quote($screenrc);
my $script = <<"EOS";
#!/usr/bin/env bash
set -u
LOG=$log_q
log(){ printf '[%s] %s\\n' "\$(date '+%Y-%m-%d %H:%M:%S')" "\$*" >> "\$LOG"; }
fail(){ log "FAILED: \$*"; exit 1; }
REPO=$repo_q
BRANCH=$branch_q
SOURCE_REL=$source_q
DEST=$install_q
GIT_BIN=$git_q
BACKUP_BASE=$backup_q
POST_UPDATE=$post_q
TMP="\${TMPDIR:-/tmp}/gsp_agent_update_\$(date +%s)_\$\$"
mkdir -p "\$TMP" "\$BACKUP_BASE" || fail "Cannot create staging or backup directories"
log "queued component=$component repo=\$REPO branch=\$BRANCH source=\$SOURCE_REL dest=\$DEST"
if ! command -v "\$GIT_BIN" >/dev/null 2>&1 && [ ! -x "\$GIT_BIN" ]; then
fail "Git executable not found: \$GIT_BIN"
fi
"\$GIT_BIN" clone --depth 1 --branch "\$BRANCH" "\$REPO" "\$TMP/repo" >> "\$LOG" 2>&1 || fail "git clone failed"
SRC="\$TMP/repo/\$SOURCE_REL"
[ -d "\$SRC" ] || fail "Source component folder missing: \$SRC"
case "\$DEST" in /*) ;; *) fail "Destination must be absolute: \$DEST" ;; esac
mkdir -p "\$DEST" || fail "Cannot create destination: \$DEST"
ARCHIVE="\$BACKUP_BASE/${component}_\$(date +%Y%m%d_%H%M%S).tar.gz"
tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -czf "\$ARCHIVE" -C "\$DEST" . >> "\$LOG" 2>&1 || fail "Backup failed"
if command -v rsync >/dev/null 2>&1; then
rsync -a --exclude='Cfg/' --exclude='ServerFiles/' --exclude='Schedule/' --exclude='logs/' --exclude='screenlogs/' --exclude='steamcmd/' --exclude='startups/' --exclude='tmp/' --exclude='shared/' --exclude='component_backups/' --exclude='*.pid' "\$SRC"/ "\$DEST"/ >> "\$LOG" 2>&1 || fail "rsync copy failed"
else
(cd "\$SRC" && tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -cf - .) | (cd "\$DEST" && tar -xf -) >> "\$LOG" 2>&1 || fail "tar copy failed"
fi
if [ -f "\$DEST/ogp_agent.pl" ]; then
perl -c "\$DEST/ogp_agent.pl" >> "\$LOG" 2>&1 || fail "Updated ogp_agent.pl failed syntax validation"
fi
if [ -n "\$POST_UPDATE" ]; then
(cd "\$DEST" && bash -lc "\$POST_UPDATE") >> "\$LOG" 2>&1 || fail "post-update command failed"
fi
log "copy complete archive=\$ARCHIVE"
sleep 2
cd "\$DEST" || exit 0
if [ -f ogp_agent_run.pid ]; then kill "\$(cat ogp_agent_run.pid)" >/dev/null 2>&1 || true; fi
if [ -f ogp_agent.pid ]; then kill "\$(cat ogp_agent.pid)" >/dev/null 2>&1 || true; fi
sleep 2
if [ -f ogp_agent_run ]; then
screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent bash ogp_agent_run -pidfile ogp_agent_run.pid >> "\$LOG" 2>&1 || true
else
screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent perl ogp_agent.pl >> "\$LOG" 2>&1 || true
fi
log "restart attempted"
rm -rf "\$TMP"
exit 0
EOS
my $fh;
if (!open($fh, '>', $script_path))
{
return component_update_response(success => 0, status => 'write_failed', message => "Cannot write updater script: $!");
}
print $fh $script;
close($fh);
chmod 0700, $script_path;
system('screen -d -m -t "component_update" -c "' . SCREENRC_FILE . '" -S component_update bash ' . component_update_shell_quote($script_path));
if ($? != 0)
{
system('bash ' . component_update_shell_quote($script_path) . ' >/dev/null 2>&1 &');
}
logger "Component update queued for $component from $repo_url branch $branch.";
return component_update_response(success => 1, status => 'queued', message => 'Component update queued on agent.', log_path => $log_path, script_path => $script_path);
}
sub agent_restart
{
return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK");

View file

@ -408,6 +408,7 @@ my $d = Frontier::Daemon::OGP::Forking->new(
fastdl_get_info => \&fastdl_get_info,
fastdl_create_config => \&fastdl_create_config,
agent_restart => \&agent_restart,
component_update => \&component_update,
scheduler_add_task => \&scheduler_add_task,
scheduler_del_task => \&scheduler_del_task,
scheduler_list_tasks => \&scheduler_list_tasks,
@ -4673,6 +4674,165 @@ sub fastdl_create_config
return 1;
}
sub component_update_shell_quote
{
my ($value) = @_;
$value = '' unless defined $value;
$value =~ s/'/'"'"'/g;
return "'" . $value . "'";
}
sub component_update_response
{
my (%data) = @_;
my %encoded = ();
foreach my $key (keys %data)
{
$encoded{$key} = encode_base64(defined $data{$key} ? $data{$key} : '', '');
}
return \%encoded;
}
sub component_update_parse_payload
{
my ($payload) = @_;
my %data = ();
foreach my $line (split(/\r?\n/, $payload || ''))
{
next if $line !~ /^([A-Za-z0-9_]+)=(.*)$/;
$data{$1} = decode_base64($2);
}
return %data;
}
sub component_update
{
return component_update_response(success => 0, status => 'bad_encryption', message => 'Bad Encryption Key')
unless(decrypt_param(pop(@_)) eq "Encryption checking OK");
my ($payload) = decrypt_params(@_);
my %cfg = component_update_parse_payload($payload);
my $component = $cfg{component} || 'linux_agent';
if ($component ne 'linux_agent' && $component ne 'windows_agent')
{
return component_update_response(success => 0, status => 'invalid_component', message => 'Invalid component.');
}
my $repo_url = $cfg{repo_url} || '';
my $branch = $cfg{branch} || '';
my $source_path = $cfg{source_path} || '';
my $install_path = $cfg{install_path} || AGENT_RUN_DIR;
my $git_path = $cfg{git_path} || 'git';
my $backup_path = $cfg{backup_path} || Path::Class::Dir->new(AGENT_RUN_DIR, 'component_backups')->stringify;
my $post_update_command = $cfg{post_update_command} || '';
if ($repo_url !~ m{^(https?://|ssh://|git@|/)} || $repo_url =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_repo', message => 'Invalid repository URL or path.');
}
if ($branch !~ /^[A-Za-z0-9._\/-]{1,128}$/)
{
return component_update_response(success => 0, status => 'invalid_branch', message => 'Invalid branch.');
}
if ($source_path eq '' || $source_path =~ /\.\./ || $source_path =~ /[\r\n\0]/ || $source_path !~ /^[A-Za-z0-9._\/-]+$/)
{
return component_update_response(success => 0, status => 'invalid_source_path', message => 'Invalid source path.');
}
if ($install_path !~ m{^/} || $install_path =~ /\.\./ || $install_path =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_install_path', message => 'Invalid install path.');
}
if ($post_update_command =~ /[\r\n\0]/)
{
return component_update_response(success => 0, status => 'invalid_post_update', message => 'Post-update command must be a single line.');
}
my $ts = time();
my $script_path = Path::Class::File->new(AGENT_RUN_DIR, "gsp_component_update_$ts.sh")->stringify;
my $log_path = Path::Class::File->new(AGENT_RUN_DIR, 'gsp_component_update.log')->stringify;
my $screenrc = SCREENRC_FILE;
my $repo_q = component_update_shell_quote($repo_url);
my $branch_q = component_update_shell_quote($branch);
my $source_q = component_update_shell_quote($source_path);
my $install_q = component_update_shell_quote($install_path);
my $git_q = component_update_shell_quote($git_path);
my $backup_q = component_update_shell_quote($backup_path);
my $post_q = component_update_shell_quote($post_update_command);
my $log_q = component_update_shell_quote($log_path);
my $screenrc_q = component_update_shell_quote($screenrc);
my $script = <<"EOS";
#!/usr/bin/env bash
set -u
LOG=$log_q
log(){ printf '[%s] %s\\n' "\$(date '+%Y-%m-%d %H:%M:%S')" "\$*" >> "\$LOG"; }
fail(){ log "FAILED: \$*"; exit 1; }
REPO=$repo_q
BRANCH=$branch_q
SOURCE_REL=$source_q
DEST=$install_q
GIT_BIN=$git_q
BACKUP_BASE=$backup_q
POST_UPDATE=$post_q
TMP="\${TMPDIR:-/tmp}/gsp_agent_update_\$(date +%s)_\$\$"
mkdir -p "\$TMP" "\$BACKUP_BASE" || fail "Cannot create staging or backup directories"
log "queued component=$component repo=\$REPO branch=\$BRANCH source=\$SOURCE_REL dest=\$DEST"
if ! command -v "\$GIT_BIN" >/dev/null 2>&1 && [ ! -x "\$GIT_BIN" ]; then
fail "Git executable not found: \$GIT_BIN"
fi
"\$GIT_BIN" clone --depth 1 --branch "\$BRANCH" "\$REPO" "\$TMP/repo" >> "\$LOG" 2>&1 || fail "git clone failed"
SRC="\$TMP/repo/\$SOURCE_REL"
[ -d "\$SRC" ] || fail "Source component folder missing: \$SRC"
case "\$DEST" in /*) ;; *) fail "Destination must be absolute: \$DEST" ;; esac
mkdir -p "\$DEST" || fail "Cannot create destination: \$DEST"
ARCHIVE="\$BACKUP_BASE/${component}_\$(date +%Y%m%d_%H%M%S).tar.gz"
tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -czf "\$ARCHIVE" -C "\$DEST" . >> "\$LOG" 2>&1 || fail "Backup failed"
if command -v rsync >/dev/null 2>&1; then
rsync -a --exclude='Cfg/' --exclude='ServerFiles/' --exclude='Schedule/' --exclude='logs/' --exclude='screenlogs/' --exclude='steamcmd/' --exclude='startups/' --exclude='tmp/' --exclude='shared/' --exclude='component_backups/' --exclude='*.pid' "\$SRC"/ "\$DEST"/ >> "\$LOG" 2>&1 || fail "rsync copy failed"
else
(cd "\$SRC" && tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -cf - .) | (cd "\$DEST" && tar -xf -) >> "\$LOG" 2>&1 || fail "tar copy failed"
fi
if [ -f "\$DEST/ogp_agent.pl" ]; then
perl -c "\$DEST/ogp_agent.pl" >> "\$LOG" 2>&1 || fail "Updated ogp_agent.pl failed syntax validation"
fi
if [ -n "\$POST_UPDATE" ]; then
(cd "\$DEST" && bash -lc "\$POST_UPDATE") >> "\$LOG" 2>&1 || fail "post-update command failed"
fi
log "copy complete archive=\$ARCHIVE"
sleep 2
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^ogp_agent\\.service'; then
systemctl restart ogp_agent.service >> "\$LOG" 2>&1 && exit 0
fi
cd "\$DEST" || exit 0
if [ -f ogp_agent_run.pid ]; then kill "\$(cat ogp_agent_run.pid)" >/dev/null 2>&1 || true; fi
if [ -f ogp_agent.pid ]; then kill "\$(cat ogp_agent.pid)" >/dev/null 2>&1 || true; fi
sleep 2
if [ -f ogp_agent_run ]; then
screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent bash ogp_agent_run -pidfile ogp_agent_run.pid >> "\$LOG" 2>&1 || true
else
screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent perl ogp_agent.pl >> "\$LOG" 2>&1 || true
fi
log "restart attempted"
rm -rf "\$TMP"
exit 0
EOS
my $fh;
if (!open($fh, '>', $script_path))
{
return component_update_response(success => 0, status => 'write_failed', message => "Cannot write updater script: $!");
}
print $fh $script;
close($fh);
chmod 0700, $script_path;
system('screen -d -m -t "component_update" -c "' . SCREENRC_FILE . '" -S component_update bash ' . component_update_shell_quote($script_path));
if ($? != 0)
{
system('bash ' . component_update_shell_quote($script_path) . ' >/dev/null 2>&1 &');
}
logger "Component update queued for $component from $repo_url branch $branch.";
return component_update_response(success => 1, status => 'queued', message => 'Component update queued on agent.', log_path => $log_path, script_path => $script_path);
}
sub agent_restart
{
return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK");

View file

@ -991,6 +991,31 @@ class OGPRemoteLibrary
return -1;
return 1;
}
public function component_update($payload)
{
$args = $this->encryptParam($payload);
$this->add_enc_chk($args);
$request = xmlrpc_encode_request("component_update", $args);
$response = $this->sendRequest($request);
if (is_array($response) && xmlrpc_is_fault($response)) {
return array('success' => false, 'status' => 'xmlrpc_fault', 'message' => isset($response['faultString']) ? $response['faultString'] : 'XML-RPC fault');
}
if (is_array($response)) {
$data = array();
foreach ((array)$response as $key => $value) {
$data[$key] = is_string($value) ? base64_decode($value) : $value;
}
if (isset($data['success'])) {
$data['success'] = in_array((string)$data['success'], array('1', 'true', 'yes'), true);
}
return $data;
}
if ($response === NULL) {
return array('success' => false, 'status' => 'agent_unreachable', 'message' => 'No response from agent.');
}
return array('success' => false, 'status' => 'unexpected_response', 'message' => (string)$response);
}
public function scheduler_list_tasks()
{

View file

@ -32,6 +32,11 @@ defined('GSP_DEFAULT_REPO_URL') || define('GSP_DEFAULT_REPO_URL', 'http://forge.
defined('GSP_DEFAULT_BRANCH') || define('GSP_DEFAULT_BRANCH', 'Panel-unstable');
defined('GSP_DEFAULT_REPO_ROOT') || define('GSP_DEFAULT_REPO_ROOT', '/var/www/html/GSP');
defined('GSP_DEFAULT_PANEL_PATH') || define('GSP_DEFAULT_PANEL_PATH', '/var/www/html/GSP/Panel');
defined('GSP_DEFAULT_WEBSITE_PATH') || define('GSP_DEFAULT_WEBSITE_PATH', '/var/www/html/GSP/Website');
defined('GSP_DEFAULT_PANEL_SOURCE') || define('GSP_DEFAULT_PANEL_SOURCE', 'Panel');
defined('GSP_DEFAULT_LINUX_AGENT_SOURCE') || define('GSP_DEFAULT_LINUX_AGENT_SOURCE', 'Agent_Linux');
defined('GSP_DEFAULT_WINDOWS_AGENT_SOURCE') || define('GSP_DEFAULT_WINDOWS_AGENT_SOURCE', 'Agent-Windows');
defined('GSP_DEFAULT_WEBSITE_SOURCE') || define('GSP_DEFAULT_WEBSITE_SOURCE', 'Website');
$gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php';
if (file_exists($gspPatchManager)) {
@ -151,11 +156,23 @@ function gsp_update_settings()
global $settings;
$repo_root = !empty($settings['gsp_update_repo_root']) ? (string)$settings['gsp_update_repo_root'] : GSP_DEFAULT_REPO_ROOT;
$panel_path = !empty($settings['gsp_update_panel_path']) ? (string)$settings['gsp_update_panel_path'] : GSP_DEFAULT_PANEL_PATH;
$website_path = !empty($settings['gsp_update_website_path']) ? (string)$settings['gsp_update_website_path'] : GSP_DEFAULT_WEBSITE_PATH;
return [
'repo_url' => !empty($settings['gsp_update_repo_url']) ? (string)$settings['gsp_update_repo_url'] : GSP_DEFAULT_REPO_URL,
'branch' => !empty($settings['gsp_update_branch']) ? (string)$settings['gsp_update_branch'] : GSP_DEFAULT_BRANCH,
'repo_root' => rtrim($repo_root, '/'),
'panel_path' => rtrim($panel_path, '/'),
'website_path' => rtrim($website_path, '/'),
'panel_source_path' => !empty($settings['gsp_update_panel_source_path']) ? trim((string)$settings['gsp_update_panel_source_path'], '/') : GSP_DEFAULT_PANEL_SOURCE,
'linux_agent_source_path' => !empty($settings['gsp_update_linux_agent_source_path']) ? trim((string)$settings['gsp_update_linux_agent_source_path'], '/') : GSP_DEFAULT_LINUX_AGENT_SOURCE,
'windows_agent_source_path' => !empty($settings['gsp_update_windows_agent_source_path']) ? trim((string)$settings['gsp_update_windows_agent_source_path'], '/') : GSP_DEFAULT_WINDOWS_AGENT_SOURCE,
'website_source_path' => !empty($settings['gsp_update_website_source_path']) ? trim((string)$settings['gsp_update_website_source_path'], '/') : GSP_DEFAULT_WEBSITE_SOURCE,
'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git',
'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE,
'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '',
'website_post_update_command' => !empty($settings['gsp_update_website_post_update_command']) ? (string)$settings['gsp_update_website_post_update_command'] : '',
'linux_agent_post_update_command' => !empty($settings['gsp_update_linux_agent_post_update_command']) ? (string)$settings['gsp_update_linux_agent_post_update_command'] : '',
'windows_agent_post_update_command' => !empty($settings['gsp_update_windows_agent_post_update_command']) ? (string)$settings['gsp_update_windows_agent_post_update_command'] : '',
'backup_before_update' => !isset($settings['gsp_update_backup_before']) ? '1' : (string)$settings['gsp_update_backup_before'],
];
}
@ -175,7 +192,7 @@ $errors[] = 'Repository source must be an http(s), ssh, git@ URL, or a safe abso
if ($is_local_path && !is_dir($repo_source)) {
$errors[] = 'Repository local path does not exist or is not a directory: ' . $repo_source;
}
if (!preg_match('/^[A-Za-z0-9._\/-]{1,128}$/', (string)$cfg['branch'])) {
if (!preg_match('/^[A-Za-z0-9._\/-]{1,128}$/', (string)$cfg['branch']) || strpos((string)$cfg['branch'], '..') !== false) {
$errors[] = 'Branch/channel contains invalid characters.';
}
foreach (['repo_root', 'panel_path'] as $key) {
@ -183,6 +200,25 @@ if (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== fals
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.';
}
}
foreach (['website_path', 'backup_path'] as $key) {
if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.';
}
}
foreach (['panel_source_path', 'linux_agent_source_path', 'windows_agent_source_path', 'website_source_path'] as $key) {
$value = isset($cfg[$key]) ? trim((string)$cfg[$key], '/') : '';
if ($value === '' || strpos($value, "\0") !== false || strpos($value, '..') !== false || !preg_match('/^[A-Za-z0-9._\/-]+$/', $value)) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.';
}
}
if (isset($cfg['git_path']) && trim((string)$cfg['git_path']) !== '' && (strpos((string)$cfg['git_path'], "\0") !== false || strpos((string)$cfg['git_path'], "\n") !== false || strpos((string)$cfg['git_path'], "\r") !== false)) {
$errors[] = 'Git executable path is invalid.';
}
foreach (['panel_post_update_command', 'website_post_update_command', 'linux_agent_post_update_command', 'windows_agent_post_update_command'] as $key) {
if (isset($cfg[$key]) && (strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], "\n") !== false || strpos((string)$cfg[$key], "\r") !== false)) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a single-line admin command.';
}
}
if (rtrim((string)$cfg['panel_path'], '/') !== rtrim((string)$cfg['repo_root'], '/') . '/Panel') {
$errors[] = 'Panel Path must point to the Panel folder inside Repository Root.';
}
@ -264,7 +300,7 @@ $cwd = getcwd();
$cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : '';
$root_path = rtrim((string)$update_cfg['repo_root'], '/');
$panel_path = rtrim((string)$update_cfg['panel_path'], '/');
$website_path = $root_path . '/Website';
$website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website');
$root_real = realpath($root_path) ?: $root_path;
$panel_real = realpath($panel_path) ?: $panel_path;
$website_real = realpath($website_path) ?: $website_path;
@ -654,14 +690,14 @@ $layout = [
'cwd' => getcwd() ?: '',
'live_gsp_root' => rtrim((string)$update_cfg['repo_root'], '/'),
'live_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'),
'live_website_path' => rtrim((string)$update_cfg['repo_root'], '/') . '/Website',
'live_website_path' => !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : (rtrim((string)$update_cfg['repo_root'], '/') . '/Website'),
'temporary_git_checkout_path' => $temp_checkout_path,
'source_root' => $source_root_real,
'source_repo_root' => $repo_root,
'source_panel_path' => $repo_root ? ($repo_root . '/Panel') : '',
'source_website_path' => $repo_root ? ($repo_root . '/Website') : '',
'source_panel_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['panel_source_path'], '/')) : '',
'source_website_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['website_source_path'], '/')) : '',
'destination_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'),
'destination_website_path' => rtrim((string)$update_cfg['repo_root'], '/') . '/Website',
'destination_website_path' => !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : (rtrim((string)$update_cfg['repo_root'], '/') . '/Website'),
];
$errors = [];
@ -1024,6 +1060,7 @@ function gsp_checkout_update_source(array $update_cfg)
{
$repo_url = (string)$update_cfg['repo_url'];
$branch = (string)$update_cfg['branch'];
$git_bin = !empty($update_cfg['git_path']) ? (string)$update_cfg['git_path'] : 'git';
$temp_dir = sys_get_temp_dir() . '/gsp_git_' . time() . '_' . mt_rand(1000, 9999);
if (!@mkdir($temp_dir, 0750, true)) {
return ['success' => false, 'error' => 'Cannot create temporary git checkout directory.'];
@ -1034,7 +1071,7 @@ return ['success' => false, 'error' => 'PHP exec() is disabled, so the updater c
}
$out = [];
$ret = 0;
$cmd = 'git clone --depth 1 --branch ' . escapeshellarg($branch) . ' ' . escapeshellarg($repo_url) . ' ' . escapeshellarg($temp_dir) . ' 2>&1';
$cmd = escapeshellarg($git_bin) . ' clone --depth 1 --branch ' . escapeshellarg($branch) . ' ' . escapeshellarg($repo_url) . ' ' . escapeshellarg($temp_dir) . ' 2>&1';
gsp_update_log('Starting configured git checkout from ' . $repo_url . ' branch ' . $branch);
exec($cmd, $out, $ret);
if ($ret !== 0) {
@ -1191,6 +1228,65 @@ if (!$preflight['success']) {
return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])];
}
$backup = gsp_create_full_backup($update_type, $ref, false);
if (!$backup['success']) {
return $backup;
}
gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}");
$temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time() . '_' . mt_rand(1000, 9999);
@mkdir($temp_dir, 0750, true);
$zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir);
if (!$zip_file) {
gsp_rmdir_recursive($temp_dir);
return ['success' => false, 'error' => 'Failed to download update ZIP from GitHub.'];
}
$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce);
@unlink($zip_file);
gsp_rmdir_recursive($temp_dir);
if (!empty($apply['restart_required'])) {
$apply['backup_dir'] = $backup['backup_dir'];
$apply['success'] = false;
return $apply;
}
if (!$apply['success']) {
return $apply;
}
$commit_after = gsp_get_git_commit();
gsp_fix_permissions(GSP_ROOT_DIR);
gsp_clear_panel_cache(GSP_PANEL_DIR);
gsp_write_version_file($ref, $update_type);
$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref;
$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref);
gsp_write_version_json($update_type, $vsource, $vversion, $commit_after);
gsp_write_last_update_markers(GSP_ROOT_DIR);
$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]);
if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) {
require_once(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php');
}
if (function_exists('updateAllPanelModules')) {
updateAllPanelModules();
}
if (function_exists('runPostUpdateOperations')) {
runPostUpdateOperations();
}
gsp_update_log("Update to {$ref} (type={$update_type}) complete");
return [
'success' => true,
'files_copied' => $apply['files_copied'],
'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0,
'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0,
'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [],
'backup_dir' => $backup['backup_dir'],
'preserved' => $apply['preserved'],
'patches' => $apply['patches'],
];
}
function gsp_do_configured_git_update(array $update_cfg, $restart_nonce = '')
{
global $db;
@ -1258,65 +1354,332 @@ return [
];
}
$backup = gsp_create_full_backup($update_type, $ref, false);
function gsp_safe_component_backup($component, $source_path, $backup_base)
{
$component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component);
$backup_base = rtrim((string)$backup_base, '/');
if ($backup_base === '' || strpos($backup_base, '/') !== 0) {
$backup_base = GSP_BACKUP_BASE;
}
if (!is_dir($backup_base) && !@mkdir($backup_base, 0755, true)) {
return ['success' => false, 'error' => 'Cannot create backup path: ' . $backup_base];
}
$ts = date('Y-m-d_H-i-s');
$backup_dir = $backup_base . '/component_' . $component . '_' . $ts;
if (!@mkdir($backup_dir, 0755, true)) {
return ['success' => false, 'error' => 'Cannot create component backup directory: ' . $backup_dir];
}
if (!is_dir($source_path)) {
return ['success' => true, 'backup_dir' => $backup_dir, 'skipped' => true];
}
$archive = $backup_dir . '/' . $component . '.tar.gz';
$archive_result = gsp_create_archive($source_path, $archive, ['./logs', './cache', './tmp', './backups', './uploads', './upload', './Cfg', './ServerFiles', './Schedule', './steamcmd', './startups']);
if (!$archive_result['success']) {
return $archive_result;
}
@file_put_contents($backup_dir . '/backup.json', json_encode([
'component' => $component,
'source_path' => $source_path,
'created_at' => date('Y-m-d H:i:s'),
'archive' => basename($archive),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive];
}
function gsp_component_source_path($source_root, $relative)
{
$relative = trim((string)$relative, '/');
if ($relative === '' || strpos($relative, '..') !== false || strpos($relative, "\0") !== false) {
return false;
}
$path = rtrim($source_root, '/') . '/' . $relative;
$real = realpath($path);
if ($real === false || strpos($real, realpath($source_root) ?: $source_root) !== 0) {
return false;
}
return $real;
}
function gsp_component_copy_tree($src_root, $dst_root, $component_label)
{
$copied = 0;
$skipped = [];
$copied_files = [];
if (!is_dir($src_root)) {
return ['success' => false, 'error' => 'Source component path not found: ' . $src_root];
}
if (!is_dir($dst_root) && !@mkdir($dst_root, 0755, true)) {
return ['success' => false, 'error' => 'Destination path could not be created: ' . $dst_root];
}
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src_root, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$protected = [
'includes/config.inc.php',
'config.inc.php',
'Cfg/',
'ServerFiles/',
'Schedule/',
'logs/',
'log/',
'backups/',
'backup/',
'cache/',
'tmp/',
'temp/',
'uploads/',
'upload/',
'home/',
'homes/',
'servers/',
'game_servers/',
'steamcmd/',
'startups/',
];
foreach ($iter as $item) {
$rel = gsp_normalize_rel(substr($item->getPathname(), strlen(rtrim($src_root, '/')) + 1));
$is_protected = false;
foreach ($protected as $prefix) {
if ($rel === rtrim($prefix, '/') || strpos($rel, $prefix) === 0) {
$is_protected = true;
break;
}
}
if ($is_protected) {
$skipped[] = $component_label . '/' . $rel;
continue;
}
$dst = rtrim($dst_root, '/') . '/' . $rel;
if ($item->isDir()) {
if (!is_dir($dst)) {
@mkdir($dst, 0755, true);
}
continue;
}
if (gsp_copy_file($item->getPathname(), $dst)) {
$copied++;
if (count($copied_files) < 200) {
$copied_files[] = $component_label . '/' . $rel;
}
}
}
return ['success' => true, 'files_copied' => $copied, 'skipped' => array_values(array_unique($skipped)), 'copied_files' => $copied_files];
}
function gsp_run_admin_post_update_command($command, $cwd)
{
$command = trim((string)$command);
if ($command === '') {
return ['success' => true, 'skipped' => true, 'output' => ''];
}
if (strpos($command, "\0") !== false || strpos($command, "\n") !== false || strpos($command, "\r") !== false) {
return ['success' => false, 'error' => 'Post-update command must be a single line.'];
}
$out = [];
$ret = 0;
exec('cd ' . escapeshellarg($cwd) . ' && ' . $command . ' 2>&1', $out, $ret);
return ['success' => $ret === 0, 'exit_code' => $ret, 'output' => implode("\n", $out), 'error' => $ret === 0 ? '' : implode("\n", $out)];
}
function gsp_do_local_component_update(array $update_cfg, array $components)
{
$allowed = array('panel', 'website');
$components = array_values(array_intersect($allowed, array_unique($components)));
if (empty($components)) {
return ['success' => true, 'files_copied' => 0, 'results' => []];
}
$validation = gsp_validate_update_settings($update_cfg);
if (!empty($validation)) {
return ['success' => false, 'error' => implode(' | ', $validation)];
}
$checkout = gsp_checkout_update_source($update_cfg);
if (!$checkout['success']) {
return $checkout;
}
$results = [];
$total = 0;
$all_skipped = [];
$all_copied = [];
$map = [
'panel' => ['source' => $update_cfg['panel_source_path'], 'dest' => $update_cfg['panel_path'], 'post' => $update_cfg['panel_post_update_command']],
'website' => ['source' => $update_cfg['website_source_path'], 'dest' => $update_cfg['website_path'], 'post' => $update_cfg['website_post_update_command']],
];
foreach ($components as $component) {
$source = gsp_component_source_path($checkout['source_root'], $map[$component]['source']);
if ($source === false) {
gsp_rmdir_recursive($checkout['temp_dir']);
return ['success' => false, 'error' => 'Source folder for ' . $component . ' is invalid or missing.'];
}
$dest = rtrim((string)$map[$component]['dest'], '/');
if (strpos($dest, '/') !== 0 || strpos($dest, '..') !== false || strpos($dest, "\0") !== false) {
gsp_rmdir_recursive($checkout['temp_dir']);
return ['success' => false, 'error' => 'Destination path for ' . $component . ' is invalid.'];
}
$backup = ['success' => true, 'backup_dir' => null];
if (!empty($update_cfg['backup_before_update'])) {
$backup = gsp_safe_component_backup($component, $dest, $update_cfg['backup_path']);
if (!$backup['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return $backup;
}
gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}");
$temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time() . '_' . mt_rand(1000, 9999);
@mkdir($temp_dir, 0750, true);
$zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir);
if (!$zip_file) {
gsp_rmdir_recursive($temp_dir);
return ['success' => false, 'error' => 'Failed to download update ZIP from GitHub.'];
}
$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce);
@unlink($zip_file);
gsp_rmdir_recursive($temp_dir);
if (!empty($apply['restart_required'])) {
$apply['backup_dir'] = $backup['backup_dir'];
$apply['success'] = false;
return $apply;
$sync = gsp_component_copy_tree($source, $dest, ucfirst($component));
if (!$sync['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return $sync;
}
if (!$apply['success']) {
return $apply;
$post = gsp_run_admin_post_update_command($map[$component]['post'], $dest);
if (!$post['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return ['success' => false, 'error' => ucfirst($component) . ' post-update command failed: ' . $post['error'], 'results' => $results];
}
$commit_after = gsp_get_git_commit();
gsp_fix_permissions(GSP_ROOT_DIR);
gsp_clear_panel_cache(GSP_PANEL_DIR);
gsp_write_version_file($ref, $update_type);
$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref;
$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref);
gsp_write_version_json($update_type, $vsource, $vversion, $commit_after);
$results[$component] = [
'source' => $source,
'destination' => $dest,
'backup_dir' => $backup['backup_dir'],
'files_copied' => $sync['files_copied'],
'skipped' => $sync['skipped'],
'post_update' => $post,
];
$total += (int)$sync['files_copied'];
$all_skipped = array_merge($all_skipped, $sync['skipped']);
$all_copied = array_merge($all_copied, $sync['copied_files']);
gsp_update_log('Local component update complete: ' . $component . ' copied=' . intval($sync['files_copied']) . ' source=' . $source . ' dest=' . $dest);
}
gsp_rmdir_recursive($checkout['temp_dir']);
gsp_write_last_update_markers($update_cfg['repo_root']);
$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]);
if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) {
require_once(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php');
if (in_array('panel', $components, true)) {
gsp_clear_panel_cache($update_cfg['panel_path']);
gsp_fix_permissions($update_cfg['panel_path']);
}
if (function_exists('updateAllPanelModules')) {
updateAllPanelModules();
}
if (function_exists('runPostUpdateOperations')) {
runPostUpdateOperations();
}
gsp_update_log("Update to {$ref} (type={$update_type}) complete");
return [
'success' => true,
'files_copied' => $apply['files_copied'],
'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0,
'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0,
'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [],
'backup_dir' => $backup['backup_dir'],
'preserved' => $apply['preserved'],
'patches' => $apply['patches'],
'files_copied' => $total,
'results' => $results,
'preserved' => array_values(array_unique($all_skipped)),
'copied_files' => array_slice(array_values(array_unique($all_copied)), 0, 200),
];
}
function gsp_update_payload_encode(array $payload)
{
$lines = [];
foreach ($payload as $key => $value) {
if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_SLASHES);
}
$lines[] = $key . '=' . base64_encode((string)$value);
}
return implode("\n", $lines);
}
function gsp_infer_agent_os(array $remote_server, $reported_os = '')
{
$reported = strtolower((string)$reported_os);
$haystack = strtolower((string)$remote_server['remote_server_name'] . ' ' . (string)$remote_server['agent_ip'] . ' ' . $reported);
if (strpos($haystack, 'windows') !== false || strpos($haystack, 'cygwin') !== false || strpos($haystack, 'mingw') !== false) {
return 'windows';
}
return 'linux';
}
function gsp_get_remote_agent_rows()
{
global $db;
$rows = $db->getRemoteServers();
return is_array($rows) ? $rows : [];
}
function gsp_dispatch_remote_agent_update(array $remote_server, array $update_cfg, $component)
{
if (!function_exists('xmlrpc_encode_request') || !function_exists('xmlrpc_decode')) {
return ['success' => false, 'status' => 'missing_xmlrpc', 'message' => 'PHP xmlrpc extension is required for remote agent updates.'];
}
require_once(GSP_PANEL_DIR . '/includes/lib_remote.php');
$component = ($component === 'windows_agent') ? 'windows_agent' : 'linux_agent';
$source_path = ($component === 'windows_agent') ? $update_cfg['windows_agent_source_path'] : $update_cfg['linux_agent_source_path'];
$post_cmd = ($component === 'windows_agent') ? $update_cfg['windows_agent_post_update_command'] : $update_cfg['linux_agent_post_update_command'];
$payload = [
'component' => $component,
'repo_url' => $update_cfg['repo_url'],
'branch' => $update_cfg['branch'],
'source_path' => $source_path,
'install_path' => '',
'git_path' => $update_cfg['git_path'],
'backup_path' => '',
'post_update_command' => $post_cmd,
'requested_at' => date('Y-m-d H:i:s'),
];
$remote = new OGPRemoteLibrary($remote_server['agent_ip'], $remote_server['agent_port'], $remote_server['encryption_key'], $remote_server['timeout']);
if ((int)$remote->status_chk() !== 1) {
return ['success' => false, 'status' => 'agent_unreachable', 'message' => 'Agent unreachable or encryption mismatch.'];
}
if (!method_exists($remote, 'component_update')) {
return ['success' => false, 'status' => 'panel_missing_rpc_wrapper', 'message' => 'Panel remote library does not expose component_update().'];
}
$response = $remote->component_update(gsp_update_payload_encode($payload));
if (is_array($response)) {
return $response;
}
return ['success' => false, 'status' => 'unexpected_response', 'message' => 'Unexpected agent response: ' . substr((string)$response, 0, 300)];
}
function gsp_do_remote_agents_update(array $update_cfg, array $remote_ids, array $component_filters)
{
global $db;
if (!function_exists('xmlrpc_encode_request') || !function_exists('xmlrpc_decode')) {
return ['success' => false, 'results' => [], 'success_count' => 0, 'message' => 'PHP xmlrpc extension is required for remote agent updates.'];
}
require_once(GSP_PANEL_DIR . '/includes/lib_remote.php');
$remote_ids = array_map('intval', $remote_ids);
$remote_ids = array_values(array_filter(array_unique($remote_ids)));
if (empty($remote_ids)) {
return ['success' => true, 'results' => [], 'message' => 'No remote agents selected.'];
}
$all = gsp_get_remote_agent_rows();
$by_id = [];
foreach ($all as $row) {
$by_id[(int)$row['remote_server_id']] = $row;
}
$results = [];
$success_count = 0;
foreach ($remote_ids as $remote_id) {
if (empty($by_id[$remote_id])) {
$results[$remote_id] = ['success' => false, 'status' => 'not_found', 'message' => 'Remote host not found.'];
continue;
}
$row = $by_id[$remote_id];
$reported_os = '';
try {
$probe = new OGPRemoteLibrary($row['agent_ip'], $row['agent_port'], $row['encryption_key'], $row['timeout']);
$reported_os = (string)$probe->what_os();
} catch (Exception $e) {
$reported_os = '';
}
$agent_os = gsp_infer_agent_os($row, $reported_os);
if ($agent_os === 'linux' && !in_array('linux_agent', $component_filters, true)) {
$results[$remote_id] = ['success' => true, 'status' => 'skipped', 'message' => 'Skipped Linux agent by filter.', 'agent_os' => $agent_os];
continue;
}
if ($agent_os === 'windows' && !in_array('windows_agent', $component_filters, true)) {
$results[$remote_id] = ['success' => true, 'status' => 'skipped', 'message' => 'Skipped Windows agent by filter.', 'agent_os' => $agent_os];
continue;
}
$component = ($agent_os === 'windows') ? 'windows_agent' : 'linux_agent';
$result = gsp_dispatch_remote_agent_update($row, $update_cfg, $component);
$result['agent_os'] = $agent_os;
$result['remote_server_name'] = $row['remote_server_name'];
$result['agent_ip'] = $row['agent_ip'];
$results[$remote_id] = $result;
if (!empty($result['success'])) {
$success_count++;
}
gsp_update_log('Remote agent update requested: id=' . $remote_id . ' component=' . $component . ' result=' . json_encode($result));
}
return ['success' => true, 'results' => $results, 'success_count' => $success_count];
}
function gsp_get_available_backups()
{
$backups = [];
@ -1947,6 +2310,17 @@ $new_cfg = [
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : '',
'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : '',
'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : '',
'website_path' => isset($_POST['gsp_update_website_path']) ? rtrim(trim((string)$_POST['gsp_update_website_path']), '/') : '',
'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : '',
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : '',
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : '',
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : '',
'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '',
'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '',
'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '',
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : '',
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : '',
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : '',
'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
];
$errors = gsp_validate_update_settings($new_cfg);
@ -1958,22 +2332,122 @@ $db->setSettings([
'gsp_update_branch' => $new_cfg['branch'],
'gsp_update_repo_root' => $new_cfg['repo_root'],
'gsp_update_panel_path' => $new_cfg['panel_path'],
'gsp_update_website_path' => $new_cfg['website_path'],
'gsp_update_panel_source_path' => $new_cfg['panel_source_path'],
'gsp_update_linux_agent_source_path' => $new_cfg['linux_agent_source_path'],
'gsp_update_windows_agent_source_path' => $new_cfg['windows_agent_source_path'],
'gsp_update_website_source_path' => $new_cfg['website_source_path'],
'gsp_update_git_path' => $new_cfg['git_path'],
'gsp_update_backup_path' => $new_cfg['backup_path'],
'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'],
'gsp_update_website_post_update_command' => $new_cfg['website_post_update_command'],
'gsp_update_linux_agent_post_update_command' => $new_cfg['linux_agent_post_update_command'],
'gsp_update_windows_agent_post_update_command' => $new_cfg['windows_agent_post_update_command'],
'gsp_update_backup_before' => $new_cfg['backup_before_update'],
]);
$settings['gsp_update_repo_url'] = $new_cfg['repo_url'];
$settings['gsp_update_branch'] = $new_cfg['branch'];
$settings['gsp_update_repo_root'] = $new_cfg['repo_root'];
$settings['gsp_update_panel_path'] = $new_cfg['panel_path'];
$settings['gsp_update_website_path'] = $new_cfg['website_path'];
$settings['gsp_update_panel_source_path'] = $new_cfg['panel_source_path'];
$settings['gsp_update_linux_agent_source_path'] = $new_cfg['linux_agent_source_path'];
$settings['gsp_update_windows_agent_source_path'] = $new_cfg['windows_agent_source_path'];
$settings['gsp_update_website_source_path'] = $new_cfg['website_source_path'];
$settings['gsp_update_git_path'] = $new_cfg['git_path'];
$settings['gsp_update_backup_path'] = $new_cfg['backup_path'];
$settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command'];
$settings['gsp_update_website_post_update_command'] = $new_cfg['website_post_update_command'];
$settings['gsp_update_linux_agent_post_update_command'] = $new_cfg['linux_agent_post_update_command'];
$settings['gsp_update_windows_agent_post_update_command'] = $new_cfg['windows_agent_post_update_command'];
$settings['gsp_update_backup_before'] = $new_cfg['backup_before_update'];
$update_cfg = gsp_update_settings();
print_success('Update settings saved.');
}
} elseif ($action === 'update_components') {
$update_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : $update_cfg['repo_url'],
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : $update_cfg['branch'],
'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : $update_cfg['repo_root'],
'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : $update_cfg['panel_path'],
'website_path' => isset($_POST['gsp_update_website_path']) ? rtrim(trim((string)$_POST['gsp_update_website_path']), '/') : $update_cfg['website_path'],
'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : $update_cfg['panel_source_path'],
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'],
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'],
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'],
'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'],
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'],
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'],
'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
];
$errors = gsp_validate_update_settings($update_cfg);
if (!empty($errors)) {
print_failure('Update settings are invalid: ' . htmlspecialchars(implode(' | ', $errors)));
} else {
$requested_components = isset($_POST['gsp_update_components']) && is_array($_POST['gsp_update_components']) ? $_POST['gsp_update_components'] : [];
$requested_components = array_values(array_intersect(array('panel', 'website', 'linux_agent', 'windows_agent'), array_map('strval', $requested_components)));
$local_components = array_values(array_intersect(array('panel', 'website'), $requested_components));
$agent_components = array_values(array_intersect(array('linux_agent', 'windows_agent'), $requested_components));
$remote_ids = isset($_POST['gsp_update_remote_ids']) && is_array($_POST['gsp_update_remote_ids']) ? array_map('intval', $_POST['gsp_update_remote_ids']) : [];
if (!empty($_POST['gsp_update_all_agents'])) {
$remote_ids = array();
foreach (gsp_get_remote_agent_rows() as $row) {
$remote_ids[] = (int)$row['remote_server_id'];
}
}
if (empty($requested_components)) {
print_failure('Select at least one component to update.');
} else {
$messages = [];
if (!empty($local_components)) {
$local_result = gsp_do_local_component_update($update_cfg, $local_components);
if (!$local_result['success']) {
print_failure('Local component update failed: ' . htmlspecialchars($local_result['error']));
} else {
$messages[] = 'Local components updated, files copied: ' . intval($local_result['files_copied']);
}
}
if (!empty($agent_components)) {
$remote_result = gsp_do_remote_agents_update($update_cfg, $remote_ids, $agent_components);
if (empty($remote_result['success']) && empty($remote_result['results'])) {
print_failure('Remote agent update failed: ' . htmlspecialchars(isset($remote_result['message']) ? $remote_result['message'] : 'Unknown error.'));
}
if (!empty($remote_result['results'])) {
echo "<h4>Remote Agent Update Results</h4><table class='center'><tr><th>Remote ID</th><th>Status</th><th>Message</th></tr>";
foreach ($remote_result['results'] as $rid => $result_row) {
$status = !empty($result_row['success']) ? (isset($result_row['status']) ? $result_row['status'] : 'queued') : (isset($result_row['status']) ? $result_row['status'] : 'failed');
$msg = isset($result_row['message']) ? $result_row['message'] : (isset($result_row['log_path']) ? $result_row['log_path'] : '');
echo "<tr><td>" . intval($rid) . "</td><td>" . htmlspecialchars($status) . "</td><td>" . htmlspecialchars($msg) . "</td></tr>";
}
echo "</table>";
}
$messages[] = 'Remote agent updates requested: ' . intval(isset($remote_result['success_count']) ? $remote_result['success_count'] : 0);
}
if (!empty($messages)) {
print_success(implode(' | ', array_map('htmlspecialchars', $messages)));
}
}
}
} elseif ($action === 'update_configured') {
$update_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : $update_cfg['repo_url'],
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : $update_cfg['branch'],
'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : $update_cfg['repo_root'],
'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : $update_cfg['panel_path'],
'website_path' => isset($_POST['gsp_update_website_path']) ? rtrim(trim((string)$_POST['gsp_update_website_path']), '/') : $update_cfg['website_path'],
'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : $update_cfg['panel_source_path'],
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'],
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'],
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'],
'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'],
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'],
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'],
'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
];
$db->setSettings([
@ -1981,9 +2455,24 @@ $db->setSettings([
'gsp_update_branch' => $update_cfg['branch'],
'gsp_update_repo_root' => $update_cfg['repo_root'],
'gsp_update_panel_path' => $update_cfg['panel_path'],
'gsp_update_website_path' => $update_cfg['website_path'],
'gsp_update_panel_source_path' => $update_cfg['panel_source_path'],
'gsp_update_linux_agent_source_path' => $update_cfg['linux_agent_source_path'],
'gsp_update_windows_agent_source_path' => $update_cfg['windows_agent_source_path'],
'gsp_update_website_source_path' => $update_cfg['website_source_path'],
'gsp_update_git_path' => $update_cfg['git_path'],
'gsp_update_backup_path' => $update_cfg['backup_path'],
'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'],
'gsp_update_website_post_update_command' => $update_cfg['website_post_update_command'],
'gsp_update_linux_agent_post_update_command' => $update_cfg['linux_agent_post_update_command'],
'gsp_update_windows_agent_post_update_command' => $update_cfg['windows_agent_post_update_command'],
'gsp_update_backup_before' => $update_cfg['backup_before_update'],
]);
if (!function_exists('gsp_do_configured_git_update')) {
$result = ['success' => false, 'error' => 'Configured Git updater helper is unavailable. Deploy the current Panel/modules/administration/panel_update.php and reload.'];
} else {
$result = gsp_do_configured_git_update($update_cfg, $restart_nonce);
}
if (!empty($result['restart_required'])) {
print_success('Updater files changed and were updated first. Restarting update with refreshed updater...');
$auto_restart_payload = ['action' => 'update_configured', 'nonce' => $result['restart_nonce']];
@ -2101,12 +2590,65 @@ echo "<tr><td><strong>Repository URL / Path</strong></td><td><input type='text'
echo "<tr><td><strong>Branch / Channel</strong></td><td><input type='text' name='gsp_update_branch' value='" . htmlspecialchars($update_cfg['branch'], ENT_QUOTES, 'UTF-8') . "' size='40'></td></tr>\n";
echo "<tr><td><strong>Repository Root</strong></td><td><input type='text' name='gsp_update_repo_root' value='" . htmlspecialchars($update_cfg['repo_root'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Panel Path</strong></td><td><input type='text' name='gsp_update_panel_path' value='" . htmlspecialchars($update_cfg['panel_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Website Path</strong></td><td><input type='text' name='gsp_update_website_path' value='" . htmlspecialchars($update_cfg['website_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Panel Source Folder</strong></td><td><input type='text' name='gsp_update_panel_source_path' value='" . htmlspecialchars($update_cfg['panel_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Linux Agent Source Folder</strong></td><td><input type='text' name='gsp_update_linux_agent_source_path' value='" . htmlspecialchars($update_cfg['linux_agent_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Windows Agent Source Folder</strong></td><td><input type='text' name='gsp_update_windows_agent_source_path' value='" . htmlspecialchars($update_cfg['windows_agent_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Website Source Folder</strong></td><td><input type='text' name='gsp_update_website_source_path' value='" . htmlspecialchars($update_cfg['website_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Git Executable</strong></td><td><input type='text' name='gsp_update_git_path' value='" . htmlspecialchars($update_cfg['git_path'], ENT_QUOTES, 'UTF-8') . "' size='45'> <small>Usually <code>git</code>.</small></td></tr>\n";
echo "<tr><td><strong>Backup Path</strong></td><td><input type='text' name='gsp_update_backup_path' value='" . htmlspecialchars($update_cfg['backup_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Backup Before Update</strong></td><td><label><input type='checkbox' name='gsp_update_backup_before' value='1' " . (!empty($update_cfg['backup_before_update']) ? "checked" : "") . "> create backup before updating</label></td></tr>\n";
echo "<tr><td><strong>Panel Post-update Command</strong></td><td><input type='text' name='gsp_update_panel_post_update_command' value='" . htmlspecialchars($update_cfg['panel_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'> <small>Admin-only dangerous command.</small></td></tr>\n";
echo "<tr><td><strong>Website Post-update Command</strong></td><td><input type='text' name='gsp_update_website_post_update_command' value='" . htmlspecialchars($update_cfg['website_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Linux Agent Post-update Command</strong></td><td><input type='text' name='gsp_update_linux_agent_post_update_command' value='" . htmlspecialchars($update_cfg['linux_agent_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Windows Agent Post-update Command</strong></td><td><input type='text' name='gsp_update_windows_agent_post_update_command' value='" . htmlspecialchars($update_cfg['windows_agent_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "</table>\n";
echo "<p>\n";
echo "<button type='submit' name='gsp_update_action' value='save_settings'>Save Settings</button> ";
echo "<button type='submit' name='gsp_update_action' value='update_configured' onclick='return confirm(\"Update Panel from the configured repository and branch?\");'>Update Panel</button>\n";
echo "</p>\n";
echo "<h3>Update Components</h3>\n";
echo "<p>Select the application components to update from the configured repository. Server homes, hosted game data, uploads, logs, cache, and agent configuration folders are protected.</p>\n";
echo "<p>";
echo "<label><input type='checkbox' name='gsp_update_components[]' value='panel'> Panel files</label> ";
echo "<label><input type='checkbox' name='gsp_update_components[]' value='website'> Website files</label> ";
echo "<label><input type='checkbox' name='gsp_update_components[]' value='linux_agent'> Linux agents</label> ";
echo "<label><input type='checkbox' name='gsp_update_components[]' value='windows_agent'> Windows agents</label> ";
echo "<label><input type='checkbox' name='gsp_update_all_agents' value='1'> All remote agents</label>";
echo "</p>\n";
$remote_rows = gsp_get_remote_agent_rows();
if (!empty($remote_rows)) {
if (!class_exists('OGPRemoteLibrary') && function_exists('xmlrpc_encode_request') && function_exists('xmlrpc_decode') && file_exists(GSP_PANEL_DIR . '/includes/lib_remote.php')) {
require_once(GSP_PANEL_DIR . '/includes/lib_remote.php');
}
echo "<table class='center'><tr><th>Select</th><th>Name</th><th>Host</th><th>Status</th><th>Reported OS</th></tr>\n";
foreach ($remote_rows as $remote_row) {
$status_text = 'not checked';
$os_text = '';
if (class_exists('OGPRemoteLibrary')) {
try {
$probe = new OGPRemoteLibrary($remote_row['agent_ip'], $remote_row['agent_port'], $remote_row['encryption_key'], 2);
$status_text = ((int)$probe->status_chk() === 1) ? 'online' : 'offline/unknown';
if ($status_text === 'online') {
$os_text = (string)$probe->what_os();
}
} catch (Exception $e) {
$status_text = 'unknown';
}
}
echo "<tr>";
echo "<td><input type='checkbox' name='gsp_update_remote_ids[]' value='" . intval($remote_row['remote_server_id']) . "'></td>";
echo "<td>" . htmlspecialchars($remote_row['remote_server_name']) . "</td>";
echo "<td>" . htmlspecialchars($remote_row['agent_ip'] . ':' . $remote_row['agent_port']) . "</td>";
echo "<td>" . htmlspecialchars($status_text) . "</td>";
echo "<td>" . htmlspecialchars($os_text !== '' ? $os_text : gsp_infer_agent_os($remote_row, '')) . "</td>";
echo "</tr>\n";
}
echo "</table>\n";
} else {
echo "<p>No remote agents are configured.</p>\n";
}
echo "<p><button type='submit' name='gsp_update_action' value='update_components' onclick='return confirm(\"Update selected components from the configured repository? Remote agents may restart.\");'>Update Selected Components</button></p>\n";
echo "</form>\n";
echo "<h3>Backup</h3>\n";

View file

@ -0,0 +1,35 @@
<?php
$settings = array();
require_once(dirname(__DIR__) . '/../administration/panel_update.php');
function gsp_update_smoke_assert($condition, $message)
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
echo "PASS: {$message}\n";
}
$cfg = gsp_update_settings();
gsp_update_smoke_assert(function_exists('gsp_do_configured_git_update'), 'configured Git updater helper is top-level');
gsp_update_smoke_assert($cfg['repo_url'] === 'http://forge.runlevelsystems.com/dev/GSP.git', 'default repo URL is Forgejo');
gsp_update_smoke_assert($cfg['branch'] === 'Panel-unstable', 'default branch is Panel-unstable');
gsp_update_smoke_assert($cfg['panel_source_path'] === 'Panel', 'default Panel source folder');
gsp_update_smoke_assert($cfg['linux_agent_source_path'] === 'Agent_Linux', 'default Linux agent source folder');
gsp_update_smoke_assert($cfg['windows_agent_source_path'] === 'Agent-Windows', 'default Windows agent source folder');
gsp_update_smoke_assert($cfg['website_source_path'] === 'Website', 'default Website source folder');
gsp_update_smoke_assert(empty(gsp_validate_update_settings($cfg)), 'default update settings validate');
$bad = $cfg;
$bad['branch'] = '../bad';
gsp_update_smoke_assert(!empty(gsp_validate_update_settings($bad)), 'invalid branch is rejected');
$tmp = sys_get_temp_dir() . '/gsp_update_smoke_' . getmypid();
@mkdir($tmp . '/Panel', 0777, true);
gsp_update_smoke_assert(gsp_component_source_path($tmp, 'Panel') !== false, 'component source path resolves under checkout');
gsp_update_smoke_assert(gsp_component_source_path($tmp, '../Panel') === false, 'component source path rejects traversal');
@rmdir($tmp . '/Panel');
@rmdir($tmp);
echo "All update config smoke tests passed.\n";

View file

@ -106,6 +106,24 @@ Useful configuration and runtime areas:
The agent also maintains screen logs and helper scripts inside its runtime area.
## Remote Git Self-Update
The Linux agent exposes the admin-only `component_update` XML-RPC method. The Panel update page uses this to queue a Git-based Linux agent update.
Flow:
1. Panel sends an encrypted payload containing repo URL, branch, source folder, optional Git path, optional backup path, and optional admin post-update command.
2. Agent validates the payload.
3. Agent writes `gsp_component_update_<timestamp>.sh` under the agent run directory.
4. The updater script runs detached in `screen`.
5. The script clones the configured branch into staging.
6. It copies only the configured Linux agent source folder, usually `Agent_Linux`.
7. It preserves `Cfg/`, `ServerFiles/`, `Schedule/`, logs, screen logs, `steamcmd/`, `startups/`, temporary folders, backups, and PID files.
8. It validates the updated `ogp_agent.pl` with `perl -c`.
9. It restarts `ogp_agent.service` through `systemd` if available, otherwise uses the existing `screen` startup fallback.
The agent returns `queued` immediately with the log path `gsp_component_update.log`. A queued response means the updater launched; check the log for final success/failure.
## Linux-Specific Notes
- The Linux agent uses `screen` and `sudo_exec_without_decrypt`.

View file

@ -123,6 +123,23 @@ Relevant functions:
The Windows scheduler implementation should remain aligned with the Linux scheduler implementation so the Panel can treat both the same way.
## Remote Git Self-Update
The Windows agent exposes the same admin-only `component_update` XML-RPC method as the Linux agent. In this repository the Windows agent is explicitly Cygwin-based, so the first implementation uses a Cygwin-compatible detached shell updater rather than a separate native PowerShell service wrapper.
Flow:
1. Panel sends an encrypted payload containing repo URL, branch, Windows agent source folder, optional Git path, optional backup path, and optional admin post-update command.
2. Agent validates the request and writes `gsp_component_update_<timestamp>.sh` under the current agent run directory, usually `/OGP`.
3. The updater runs detached in `screen`.
4. The updater clones the configured branch into staging.
5. It copies only the configured Windows agent source folder, usually `Agent-Windows`.
6. It preserves `Cfg/`, `ServerFiles/`, `Schedule/`, logs, screen logs, `steamcmd/`, `startups/`, temporary folders, backups, and PID files.
7. It validates `ogp_agent.pl` with `perl -c`.
8. It restarts the agent using the existing Cygwin/screen fallback.
The immediate response is `queued` with the agent-side log path `gsp_component_update.log`.
## Windows-Specific Notes
- Path conversion between Cygwin and native Windows paths matters during startup.

View file

@ -55,6 +55,7 @@ Panel module
| `start_fastdl` / `stop_fastdl` / `restart_fastdl` / `fastdl_status` | FastDL service management | `fast_download` | Source/GoldSrc web distribution support. |
| `scheduler_*` methods | Task list and task CRUD | `cron` | Agent-owned scheduler implementation. |
| `agent_restart` | Restart the agent itself | Admin maintenance | Node maintenance action. |
| `component_update` | Queue a Git-based agent code update | Update module | Admin-only. Uses encrypted XML-RPC, validates repo/branch/source/destination, launches a detached updater, preserves agent config/runtime/server data, and returns queued status plus an agent log path. |
| `shell_action` | Run a shell action in a controlled way | Advanced agent operations | Should remain tightly permissioned. |
| `send_steam_guard_code` | Submit Steam Guard code | Steam authenticated installs | Used for authenticated SteamCMD workflows. |
| `steam_workshop` / `get_workshop_mods_info` | Workshop-related helper calls | Workshop/content flows | Active in current module work, but still being consolidated. |
@ -140,6 +141,23 @@ Panel
-> refresh dedicated preformatted log panel via AJAX
```
### Remote Agent Component Update
```text
Panel update page
-> component_update(payload)
Agent
-> validate repo, branch, source folder, destination
-> write detached updater script
-> return queued + log path
Updater script
-> clone/fetch repository into staging
-> copy Agent_Linux or Agent-Windows folder only
-> preserve Cfg, ServerFiles, Schedule, logs, steamcmd, startups, pid files
-> validate ogp_agent.pl
-> restart agent through systemd or screen fallback
```
## Error Handling Notes
- Do not treat query failure as a start failure by itself.

View file

@ -124,6 +124,16 @@ This file is the first stop for future Codex sessions working in this repository
3. Check the user/admin content pages.
4. Check whether the action should be treated as install, update, or uninstall.
### Debug Panel or Agent updates
1. Read `docs/modules/UPDATE.md`.
2. Check `Panel/modules/update/update.php`.
3. Check `Panel/modules/administration/panel_update.php`.
4. Check `Panel/includes/lib_remote.php` for the `component_update` wrapper.
5. Check both `Agent_Linux/ogp_agent.pl` and `Agent-Windows/ogp_agent.pl` for the `component_update` RPC.
6. Remember that local Panel/Website updates and remote agent updates both clone a configured Git branch into staging and copy only configured component folders.
7. Never let updater logic delete server homes, game install folders, user data, agent `Cfg/`, logs, uploads, backups, or runtime PID files.
## Things Already Investigated
The repository has already been mapped in these areas:

View file

@ -26,6 +26,14 @@ The admin page stores these settings in the Panel settings table:
- `gsp_update_branch`
- `gsp_update_repo_root`
- `gsp_update_panel_path`
- `gsp_update_website_path`
- `gsp_update_panel_source_path`
- `gsp_update_linux_agent_source_path`
- `gsp_update_windows_agent_source_path`
- `gsp_update_website_source_path`
- `gsp_update_git_path`
- `gsp_update_backup_path`
- optional admin-only post-update commands per component
- `gsp_update_backup_before`
Defaults:
@ -34,6 +42,11 @@ Defaults:
- Branch: `Panel-unstable`
- Repository Root: `/var/www/html/GSP`
- Panel Path: `/var/www/html/GSP/Panel`
- Website Path: `/var/www/html/GSP/Website`
- Panel Source Folder: `Panel`
- Linux Agent Source Folder: `Agent_Linux`
- Windows Agent Source Folder: `Agent-Windows`
- Website Source Folder: `Website`
- Backup Before Update: enabled
Important implementation note:
@ -41,6 +54,7 @@ Important implementation note:
- `gsp_update_settings()` and `gsp_validate_update_settings()` are defined at top level in `Panel/modules/administration/panel_update.php`.
- These helpers must not be nested inside another function. A previous bad edit placed `gsp_update_settings()` inside `gsp_get_git_commit()`, which caused a fatal error when the update page called the helper before `gsp_get_git_commit()` had ever executed.
- If the update page throws `Call to undefined function gsp_update_settings()`, first verify the deployed `Panel/modules/administration/panel_update.php` matches the repository version and that this helper exists near the top of the file before `gsp_panel_update_section()` is called.
- `gsp_do_configured_git_update()` must also remain top-level. A bad edit placed it inside `gsp_do_update()`, so `/home.php?m=update` called an undefined function until a legacy GitHub update path happened to execute first.
## Update Flow
@ -54,6 +68,49 @@ Important implementation note:
8. Run module updates/post-update hooks.
9. Write version metadata and `LAST_UPDATE.txt`.
## Component Updates
The update page can update selected components from one repository:
- Panel files
- Website files
- Linux agents
- Windows/Cygwin agents
Local Panel/Website updates clone the configured repository into a temporary checkout and copy only the configured component source folder into the configured destination path. Protected folders and files are not overwritten:
- `includes/config.inc.php`
- `Cfg/`
- `ServerFiles/`
- `Schedule/`
- `logs/`
- `screenlogs/`
- `cache/`
- `tmp/`
- `uploads/`
- `backups/`
- `steamcmd/`
- `startups/`
- PID files
Remote agent updates use the encrypted Panel-Agent XML-RPC channel and the `component_update` RPC. The agent writes a detached updater script, clones the repo to staging, backs up the current agent code, copies only the configured agent source folder, validates `ogp_agent.pl`, then restarts through `systemd` when available or the existing `screen` fallback.
Remote update status is queued/asynchronous. The first response confirms that the update was accepted and gives the agent-side log path.
Remote updates require PHP XML-RPC on the Panel host. If the extension is missing, the update page still loads and reports a clean `missing_xmlrpc` error when a remote agent update is requested.
## Smoke Tests
Useful validation commands:
```bash
php -l Panel/modules/administration/panel_update.php
php -l Panel/modules/update/update.php
php -l Panel/includes/lib_remote.php
php Panel/modules/update/tests/update_config_smoke.php
perl -c ogp_agent.pl # run from each installed agent directory with dependencies present
```
## Diagnostics
Apache and SSL checks are diagnostics only. Missing SSL certificates do not block Panel updates.
@ -68,6 +125,7 @@ The old repeated SSL vhost disable buttons are not part of the primary update pa
## Repair Notes
- The update page fatal `Call to undefined function gsp_update_settings()` means the deployed `Panel/modules/administration/panel_update.php` is missing the top-level helper or is not the current repository copy.
- The update page fatal `Call to undefined function gsp_do_configured_git_update()` means the configured Git update helper is missing or nested inside another helper in the deployed `panel_update.php`.
- `Panel/modules/update/update.php` only loads `Panel/modules/administration/panel_update.php` and calls `gsp_panel_update_section()`.
- The configured update action uses `git clone --depth 1 --branch <configured branch> <configured repository source> <temporary checkout>`.
- Clone failures are logged to `logs/update_trace.log` with the configured repository source and branch.

View file

@ -43,13 +43,27 @@ Panel update and patch management.
## Missing Functionality
- clearer update history and rollback guidance
- richer update history and rollback guidance
- live progress polling for asynchronous remote agent updates
## Suggested Future Improvements
- keep admin-only and document carefully
- add status polling for `component_update` logs after the agent has restarted
## Recommendation
- Keep
## Current GSP Additions
The primary update implementation lives in `Panel/modules/administration/panel_update.php` and is exposed by `Panel/modules/update/update.php`.
Current update targets:
- Panel files
- Website files
- Linux agents
- Windows/Cygwin agents
The updater uses a single configured Git repository with component source folders such as `Panel`, `Website`, `Agent_Linux`, and `Agent-Windows`. Remote agents are updated through the encrypted `component_update` XML-RPC method and preserve hosted game data and agent configuration folders.