diff --git a/Agent-Windows/ogp_agent.pl b/Agent-Windows/ogp_agent.pl index c2ea2f00..626112b6 100644 --- a/Agent-Windows/ogp_agent.pl +++ b/Agent-Windows/ogp_agent.pl @@ -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"); diff --git a/Agent_Linux/ogp_agent.pl b/Agent_Linux/ogp_agent.pl index 1a90a487..573269a2 100644 --- a/Agent_Linux/ogp_agent.pl +++ b/Agent_Linux/ogp_agent.pl @@ -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"); diff --git a/Panel/includes/lib_remote.php b/Panel/includes/lib_remote.php index 77f32468..ce0c691c 100644 --- a/Panel/includes/lib_remote.php +++ b/Panel/includes/lib_remote.php @@ -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() { diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php index bb6f0751..7187edca 100644 --- a/Panel/modules/administration/panel_update.php +++ b/Panel/modules/administration/panel_update.php @@ -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 "

Remote Agent Update Results

"; +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 ""; +} +echo "
Remote IDStatusMessage
" . intval($rid) . "" . htmlspecialchars($status) . "" . htmlspecialchars($msg) . "
"; +} +$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 "Repository URL / PathBranch / Channel\n"; echo "Repository Root\n"; echo "Panel Path\n"; +echo "Website Path\n"; +echo "Panel Source Folder\n"; +echo "Linux Agent Source Folder\n"; +echo "Windows Agent Source Folder\n"; +echo "Website Source Folder\n"; +echo "Git Executable Usually git.\n"; +echo "Backup Path\n"; echo "Backup Before Update\n"; +echo "Panel Post-update Command Admin-only dangerous command.\n"; +echo "Website Post-update Command\n"; +echo "Linux Agent Post-update Command\n"; +echo "Windows Agent Post-update Command\n"; echo "\n"; echo "

\n"; echo " "; echo "\n"; echo "

\n"; +echo "

Update Components

\n"; +echo "

Select the application components to update from the configured repository. Server homes, hosted game data, uploads, logs, cache, and agent configuration folders are protected.

\n"; +echo "

"; +echo " "; +echo " "; +echo " "; +echo " "; +echo ""; +echo "

\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 "\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 ""; +echo ""; +echo ""; +echo ""; +echo ""; +echo ""; +echo "\n"; +} +echo "
SelectNameHostStatusReported OS
" . htmlspecialchars($remote_row['remote_server_name']) . "" . htmlspecialchars($remote_row['agent_ip'] . ':' . $remote_row['agent_port']) . "" . htmlspecialchars($status_text) . "" . htmlspecialchars($os_text !== '' ? $os_text : gsp_infer_agent_os($remote_row, '')) . "
\n"; +} else { +echo "

No remote agents are configured.

\n"; +} +echo "

\n"; echo "\n"; echo "

Backup

\n"; diff --git a/Panel/modules/update/tests/update_config_smoke.php b/Panel/modules/update/tests/update_config_smoke.php new file mode 100644 index 00000000..2c4425fa --- /dev/null +++ b/Panel/modules/update/tests/update_config_smoke.php @@ -0,0 +1,35 @@ +.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`. diff --git a/docs/agents/WINDOWS_AGENT.md b/docs/agents/WINDOWS_AGENT.md index f60dd335..4308d349 100644 --- a/docs/agents/WINDOWS_AGENT.md +++ b/docs/agents/WINDOWS_AGENT.md @@ -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_.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. diff --git a/docs/architecture/API_REFERENCE.md b/docs/architecture/API_REFERENCE.md index 50a6aed1..1d1b2c00 100644 --- a/docs/architecture/API_REFERENCE.md +++ b/docs/architecture/API_REFERENCE.md @@ -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. diff --git a/docs/development/CODEX_GUIDE.md b/docs/development/CODEX_GUIDE.md index 1b953b72..1b13c35f 100644 --- a/docs/development/CODEX_GUIDE.md +++ b/docs/development/CODEX_GUIDE.md @@ -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: diff --git a/docs/modules/UPDATE.md b/docs/modules/UPDATE.md index 07e9473e..daaeb9b6 100644 --- a/docs/modules/UPDATE.md +++ b/docs/modules/UPDATE.md @@ -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 `. - Clone failures are logged to `logs/update_trace.log` with the configured repository source and branch. diff --git a/docs/modules/update.md b/docs/modules/update.md index c7d89511..badc74c9 100644 --- a/docs/modules/update.md +++ b/docs/modules/update.md @@ -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.