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_get_info => \&fastdl_get_info,
fastdl_create_config => \&fastdl_create_config, fastdl_create_config => \&fastdl_create_config,
agent_restart => \&agent_restart, agent_restart => \&agent_restart,
component_update => \&component_update,
scheduler_add_task => \&scheduler_add_task, scheduler_add_task => \&scheduler_add_task,
scheduler_del_task => \&scheduler_del_task, scheduler_del_task => \&scheduler_del_task,
scheduler_list_tasks => \&scheduler_list_tasks, scheduler_list_tasks => \&scheduler_list_tasks,
@ -3498,6 +3499,162 @@ sub fastdl_create_config
return 1; 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 sub agent_restart
{ {
return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); 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_get_info => \&fastdl_get_info,
fastdl_create_config => \&fastdl_create_config, fastdl_create_config => \&fastdl_create_config,
agent_restart => \&agent_restart, agent_restart => \&agent_restart,
component_update => \&component_update,
scheduler_add_task => \&scheduler_add_task, scheduler_add_task => \&scheduler_add_task,
scheduler_del_task => \&scheduler_del_task, scheduler_del_task => \&scheduler_del_task,
scheduler_list_tasks => \&scheduler_list_tasks, scheduler_list_tasks => \&scheduler_list_tasks,
@ -4673,6 +4674,165 @@ sub fastdl_create_config
return 1; 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 sub agent_restart
{ {
return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK");

View file

@ -991,6 +991,31 @@ class OGPRemoteLibrary
return -1; return -1;
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() 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_BRANCH') || define('GSP_DEFAULT_BRANCH', 'Panel-unstable');
defined('GSP_DEFAULT_REPO_ROOT') || define('GSP_DEFAULT_REPO_ROOT', '/var/www/html/GSP'); 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_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'; $gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php';
if (file_exists($gspPatchManager)) { if (file_exists($gspPatchManager)) {
@ -151,11 +156,23 @@ function gsp_update_settings()
global $settings; global $settings;
$repo_root = !empty($settings['gsp_update_repo_root']) ? (string)$settings['gsp_update_repo_root'] : GSP_DEFAULT_REPO_ROOT; $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; $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 [ return [
'repo_url' => !empty($settings['gsp_update_repo_url']) ? (string)$settings['gsp_update_repo_url'] : GSP_DEFAULT_REPO_URL, '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, 'branch' => !empty($settings['gsp_update_branch']) ? (string)$settings['gsp_update_branch'] : GSP_DEFAULT_BRANCH,
'repo_root' => rtrim($repo_root, '/'), 'repo_root' => rtrim($repo_root, '/'),
'panel_path' => rtrim($panel_path, '/'), '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'], '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)) { if ($is_local_path && !is_dir($repo_source)) {
$errors[] = 'Repository local path does not exist or is not a directory: ' . $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.'; $errors[] = 'Branch/channel contains invalid characters.';
} }
foreach (['repo_root', 'panel_path'] as $key) { 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.'; $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') { if (rtrim((string)$cfg['panel_path'], '/') !== rtrim((string)$cfg['repo_root'], '/') . '/Panel') {
$errors[] = 'Panel Path must point to the Panel folder inside Repository Root.'; $errors[] = 'Panel Path must point to the Panel folder inside Repository Root.';
} }
@ -264,7 +300,7 @@ $cwd = getcwd();
$cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : ''; $cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : '';
$root_path = rtrim((string)$update_cfg['repo_root'], '/'); $root_path = rtrim((string)$update_cfg['repo_root'], '/');
$panel_path = rtrim((string)$update_cfg['panel_path'], '/'); $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; $root_real = realpath($root_path) ?: $root_path;
$panel_real = realpath($panel_path) ?: $panel_path; $panel_real = realpath($panel_path) ?: $panel_path;
$website_real = realpath($website_path) ?: $website_path; $website_real = realpath($website_path) ?: $website_path;
@ -654,14 +690,14 @@ $layout = [
'cwd' => getcwd() ?: '', 'cwd' => getcwd() ?: '',
'live_gsp_root' => rtrim((string)$update_cfg['repo_root'], '/'), 'live_gsp_root' => rtrim((string)$update_cfg['repo_root'], '/'),
'live_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'), '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, 'temporary_git_checkout_path' => $temp_checkout_path,
'source_root' => $source_root_real, 'source_root' => $source_root_real,
'source_repo_root' => $repo_root, 'source_repo_root' => $repo_root,
'source_panel_path' => $repo_root ? ($repo_root . '/Panel') : '', 'source_panel_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['panel_source_path'], '/')) : '',
'source_website_path' => $repo_root ? ($repo_root . '/Website') : '', 'source_website_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['website_source_path'], '/')) : '',
'destination_panel_path' => rtrim((string)$update_cfg['panel_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 = []; $errors = [];
@ -1024,6 +1060,7 @@ function gsp_checkout_update_source(array $update_cfg)
{ {
$repo_url = (string)$update_cfg['repo_url']; $repo_url = (string)$update_cfg['repo_url'];
$branch = (string)$update_cfg['branch']; $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); $temp_dir = sys_get_temp_dir() . '/gsp_git_' . time() . '_' . mt_rand(1000, 9999);
if (!@mkdir($temp_dir, 0750, true)) { if (!@mkdir($temp_dir, 0750, true)) {
return ['success' => false, 'error' => 'Cannot create temporary git checkout directory.']; 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 = []; $out = [];
$ret = 0; $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); gsp_update_log('Starting configured git checkout from ' . $repo_url . ' branch ' . $branch);
exec($cmd, $out, $ret); exec($cmd, $out, $ret);
if ($ret !== 0) { if ($ret !== 0) {
@ -1191,6 +1228,65 @@ if (!$preflight['success']) {
return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])]; 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 = '') function gsp_do_configured_git_update(array $update_cfg, $restart_nonce = '')
{ {
global $db; 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']) { if (!$backup['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return $backup; 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.'];
} }
$sync = gsp_component_copy_tree($source, $dest, ucfirst($component));
$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce); if (!$sync['success']) {
@unlink($zip_file); gsp_rmdir_recursive($checkout['temp_dir']);
gsp_rmdir_recursive($temp_dir); return $sync;
if (!empty($apply['restart_required'])) {
$apply['backup_dir'] = $backup['backup_dir'];
$apply['success'] = false;
return $apply;
} }
if (!$apply['success']) { $post = gsp_run_admin_post_update_command($map[$component]['post'], $dest);
return $apply; if (!$post['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return ['success' => false, 'error' => ucfirst($component) . ' post-update command failed: ' . $post['error'], 'results' => $results];
} }
$results[$component] = [
$commit_after = gsp_get_git_commit(); 'source' => $source,
gsp_fix_permissions(GSP_ROOT_DIR); 'destination' => $dest,
gsp_clear_panel_cache(GSP_PANEL_DIR); 'backup_dir' => $backup['backup_dir'],
gsp_write_version_file($ref, $update_type); 'files_copied' => $sync['files_copied'],
$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref; 'skipped' => $sync['skipped'],
$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref); 'post_update' => $post,
gsp_write_version_json($update_type, $vsource, $vversion, $commit_after); ];
$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']); gsp_write_last_update_markers($update_cfg['repo_root']);
$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); if (in_array('panel', $components, true)) {
gsp_clear_panel_cache($update_cfg['panel_path']);
if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) { gsp_fix_permissions($update_cfg['panel_path']);
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 [ return [
'success' => true, 'success' => true,
'files_copied' => $apply['files_copied'], 'files_copied' => $total,
'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0, 'results' => $results,
'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0, 'preserved' => array_values(array_unique($all_skipped)),
'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [], 'copied_files' => array_slice(array_values(array_unique($all_copied)), 0, 200),
'backup_dir' => $backup['backup_dir'],
'preserved' => $apply['preserved'],
'patches' => $apply['patches'],
]; ];
} }
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() function gsp_get_available_backups()
{ {
$backups = []; $backups = [];
@ -1947,6 +2310,17 @@ $new_cfg = [
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : '', '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']), '/') : '', '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']), '/') : '', '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', 'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
]; ];
$errors = gsp_validate_update_settings($new_cfg); $errors = gsp_validate_update_settings($new_cfg);
@ -1958,22 +2332,122 @@ $db->setSettings([
'gsp_update_branch' => $new_cfg['branch'], 'gsp_update_branch' => $new_cfg['branch'],
'gsp_update_repo_root' => $new_cfg['repo_root'], 'gsp_update_repo_root' => $new_cfg['repo_root'],
'gsp_update_panel_path' => $new_cfg['panel_path'], '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'], 'gsp_update_backup_before' => $new_cfg['backup_before_update'],
]); ]);
$settings['gsp_update_repo_url'] = $new_cfg['repo_url']; $settings['gsp_update_repo_url'] = $new_cfg['repo_url'];
$settings['gsp_update_branch'] = $new_cfg['branch']; $settings['gsp_update_branch'] = $new_cfg['branch'];
$settings['gsp_update_repo_root'] = $new_cfg['repo_root']; $settings['gsp_update_repo_root'] = $new_cfg['repo_root'];
$settings['gsp_update_panel_path'] = $new_cfg['panel_path']; $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']; $settings['gsp_update_backup_before'] = $new_cfg['backup_before_update'];
$update_cfg = gsp_update_settings(); $update_cfg = gsp_update_settings();
print_success('Update settings saved.'); 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') { } elseif ($action === 'update_configured') {
$update_cfg = [ $update_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : $update_cfg['repo_url'], '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'], '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'], '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'], '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', 'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
]; ];
$db->setSettings([ $db->setSettings([
@ -1981,9 +2455,24 @@ $db->setSettings([
'gsp_update_branch' => $update_cfg['branch'], 'gsp_update_branch' => $update_cfg['branch'],
'gsp_update_repo_root' => $update_cfg['repo_root'], 'gsp_update_repo_root' => $update_cfg['repo_root'],
'gsp_update_panel_path' => $update_cfg['panel_path'], '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'], '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); $result = gsp_do_configured_git_update($update_cfg, $restart_nonce);
}
if (!empty($result['restart_required'])) { if (!empty($result['restart_required'])) {
print_success('Updater files changed and were updated first. Restarting update with refreshed updater...'); print_success('Updater files changed and were updated first. Restarting update with refreshed updater...');
$auto_restart_payload = ['action' => 'update_configured', 'nonce' => $result['restart_nonce']]; $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>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>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>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>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 "</table>\n";
echo "<p>\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='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 "<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 "</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 "</form>\n";
echo "<h3>Backup</h3>\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. 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 ## Linux-Specific Notes
- The Linux agent uses `screen` and `sudo_exec_without_decrypt`. - 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. 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 ## Windows-Specific Notes
- Path conversion between Cygwin and native Windows paths matters during startup. - 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. | | `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. | | `scheduler_*` methods | Task list and task CRUD | `cron` | Agent-owned scheduler implementation. |
| `agent_restart` | Restart the agent itself | Admin maintenance | Node maintenance action. | | `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. | | `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. | | `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. | | `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 -> 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 ## Error Handling Notes
- Do not treat query failure as a start failure by itself. - 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. 3. Check the user/admin content pages.
4. Check whether the action should be treated as install, update, or uninstall. 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 ## Things Already Investigated
The repository has already been mapped in these areas: 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_branch`
- `gsp_update_repo_root` - `gsp_update_repo_root`
- `gsp_update_panel_path` - `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` - `gsp_update_backup_before`
Defaults: Defaults:
@ -34,6 +42,11 @@ Defaults:
- Branch: `Panel-unstable` - Branch: `Panel-unstable`
- Repository Root: `/var/www/html/GSP` - Repository Root: `/var/www/html/GSP`
- Panel Path: `/var/www/html/GSP/Panel` - 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 - Backup Before Update: enabled
Important implementation note: 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`. - `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. - 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. - 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 ## Update Flow
@ -54,6 +68,49 @@ Important implementation note:
8. Run module updates/post-update hooks. 8. Run module updates/post-update hooks.
9. Write version metadata and `LAST_UPDATE.txt`. 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 ## Diagnostics
Apache and SSL checks are diagnostics only. Missing SSL certificates do not block Panel updates. 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 ## 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_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()`. - `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>`. - 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. - 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 ## 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 ## Suggested Future Improvements
- keep admin-only and document carefully - keep admin-only and document carefully
- add status polling for `component_update` logs after the agent has restarted
## Recommendation ## Recommendation
- Keep - 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.