foxed update and stsrt issues

This commit is contained in:
Frank Harris 2026-06-05 14:39:10 -05:00
parent c687165132
commit c195c0930b
7 changed files with 544 additions and 70 deletions

View file

@ -27,6 +27,11 @@ defined('GSP_EXPECTED_PANEL') || define('GSP_EXPECTED_PANEL', GSP_EXPECTED_ROOT
defined('GSP_EXPECTED_WEBSITE') || define('GSP_EXPECTED_WEBSITE', GSP_EXPECTED_ROOT . '/Website');
defined('GSP_CANONICAL_TIMESTAMP_FILE') || define('GSP_CANONICAL_TIMESTAMP_FILE', GSP_WEBSITE_DIR . '/timestamp.txt');
defined('GSP_BILLING_TIMESTAMP_FILE') || define('GSP_BILLING_TIMESTAMP_FILE', GSP_PANEL_DIR . '/modules/billing/timestamp.txt');
defined('GSP_LAST_UPDATE_FILE') || define('GSP_LAST_UPDATE_FILE', GSP_ROOT_DIR . '/LAST_UPDATE.txt');
defined('GSP_DEFAULT_REPO_URL') || define('GSP_DEFAULT_REPO_URL', 'http://forge.runlevelsystems.com/dev/GSP.git');
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');
$gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php';
if (file_exists($gspPatchManager)) {
@ -129,6 +134,41 @@ $repo_root = gsp_detect_repo_root();
if (!$repo_root || !function_exists('exec')) {
return null;
}
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;
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, '/'),
'backup_before_update' => !isset($settings['gsp_update_backup_before']) ? '1' : (string)$settings['gsp_update_backup_before'],
];
}
function gsp_validate_update_settings(array $cfg)
{
$errors = [];
if (!preg_match('/^https?:\/\/[^ \t\r\n]+\.git$/i', (string)$cfg['repo_url'])
&& !preg_match('/^(?:ssh:\/\/|git@)[^ \t\r\n]+$/i', (string)$cfg['repo_url'])) {
$errors[] = 'Repository URL must be an http(s), ssh, or git@ URL.';
}
if (!preg_match('/^[A-Za-z0-9._\/-]{1,128}$/', (string)$cfg['branch'])) {
$errors[] = 'Branch/channel contains invalid characters.';
}
foreach (['repo_root', 'panel_path'] as $key) {
if (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.';
}
}
if (rtrim((string)$cfg['panel_path'], '/') !== rtrim((string)$cfg['repo_root'], '/') . '/Panel') {
$errors[] = 'Panel Path must point to the Panel folder inside Repository Root.';
}
return $errors;
}
$out = [];
$ret = 0;
exec('git -C ' . escapeshellarg($repo_root) . ' rev-parse HEAD 2>/dev/null', $out, $ret);
@ -207,58 +247,50 @@ return json_decode($data, true);
return false;
}
function gsp_preflight_check()
function gsp_preflight_check(array $update_cfg = null)
{
$errors = [];
$warnings = [];
$update_cfg = $update_cfg ?: gsp_update_settings();
$cwd = getcwd();
$cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : '';
$root_real = realpath(GSP_ROOT_DIR) ?: GSP_ROOT_DIR;
$panel_real = realpath(GSP_PANEL_DIR) ?: GSP_PANEL_DIR;
$website_real = realpath(GSP_WEBSITE_DIR) ?: GSP_WEBSITE_DIR;
$expected_root_real = realpath(GSP_EXPECTED_ROOT) ?: GSP_EXPECTED_ROOT;
$expected_panel_real = realpath(GSP_EXPECTED_PANEL) ?: GSP_EXPECTED_PANEL;
$expected_website_real = realpath(GSP_EXPECTED_WEBSITE) ?: GSP_EXPECTED_WEBSITE;
$root_path = rtrim((string)$update_cfg['repo_root'], '/');
$panel_path = rtrim((string)$update_cfg['panel_path'], '/');
$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;
$layout = [
'cwd' => $cwd,
'cwd_real' => $cwd_real,
'expected_root' => GSP_EXPECTED_ROOT,
'expected_panel' => GSP_EXPECTED_PANEL,
'expected_website' => GSP_EXPECTED_WEBSITE,
'gsp_root' => GSP_ROOT_DIR,
'expected_root' => $root_path,
'expected_panel' => $panel_path,
'expected_website' => $website_path,
'gsp_root' => $root_path,
'gsp_root_real' => $root_real,
'panel_dir' => GSP_PANEL_DIR,
'panel_dir' => $panel_path,
'panel_dir_real' => $panel_real,
'website_dir' => GSP_WEBSITE_DIR,
'website_dir' => $website_path,
'website_dir_real' => $website_real,
'backup_dir' => GSP_BACKUP_BASE,
'config_file' => GSP_PANEL_DIR . '/includes/config.inc.php',
'destination_panel' => GSP_PANEL_DIR,
'destination_website' => GSP_WEBSITE_DIR,
'config_file' => $panel_path . '/includes/config.inc.php',
'destination_panel' => $panel_path,
'destination_website' => $website_path,
];
if (!$layout['cwd']) {
$errors[] = 'Unable to read current working directory.';
} elseif (strpos($cwd_real, $panel_real) !== 0) {
$errors[] = 'Current working directory must be under live Panel path: ' . $panel_real;
$warnings[] = 'Current working directory is not under configured Panel path: ' . $panel_real;
}
if (!is_dir(GSP_ROOT_DIR)) {
if (!is_dir($root_path)) {
$errors[] = 'Detected GSP root path is missing.';
}
if ($root_real !== $expected_root_real) {
$errors[] = 'Detected GSP root does not match expected live root: ' . GSP_EXPECTED_ROOT;
}
if (!is_dir(GSP_PANEL_DIR)) {
if (!is_dir($panel_path)) {
$errors[] = 'Panel directory is missing.';
}
if ($panel_real !== $expected_panel_real) {
$errors[] = 'Detected Panel path does not match expected live Panel path: ' . GSP_EXPECTED_PANEL;
}
if (!is_dir(GSP_WEBSITE_DIR)) {
$errors[] = 'Website directory is missing.';
}
if ($website_real !== $expected_website_real) {
$errors[] = 'Detected Website path does not match expected live Website path: ' . GSP_EXPECTED_WEBSITE;
if (!is_dir($website_path)) {
$warnings[] = 'Website directory is missing. Panel updates can still continue, but Website files will not sync cleanly.';
}
if (!file_exists($layout['config_file'])) {
$errors[] = 'Panel includes/config.inc.php was not found and cannot be preserved.';
@ -269,7 +301,7 @@ $errors[] = 'Backups directory is missing and cannot be created.';
}
}
foreach ([GSP_ROOT_DIR, GSP_PANEL_DIR, GSP_WEBSITE_DIR, GSP_BACKUP_BASE] as $path) {
foreach ([$root_path, $panel_path, GSP_BACKUP_BASE] as $path) {
if (!is_writable($path)) {
$errors[] = 'Path is not writable: ' . $path;
}
@ -593,8 +625,9 @@ $source_root = $subdirs[0];
return ['success' => true, 'temp_dir' => $temp_dir, 'source_root' => $source_root];
}
function gsp_resolve_source_layout($temp_checkout_path, $source_root)
function gsp_resolve_source_layout($temp_checkout_path, $source_root, array $update_cfg = null)
{
$update_cfg = $update_cfg ?: gsp_update_settings();
$source_root_real = realpath($source_root) ?: $source_root;
$candidates = [$source_root_real];
if (basename($source_root_real) === 'Panel' || basename($source_root_real) === 'Website') {
@ -611,16 +644,16 @@ break;
$layout = [
'cwd' => getcwd() ?: '',
'live_gsp_root' => GSP_ROOT_DIR,
'live_panel_path' => GSP_PANEL_DIR,
'live_website_path' => GSP_WEBSITE_DIR,
'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',
'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') : '',
'destination_panel_path' => GSP_PANEL_DIR,
'destination_website_path' => GSP_WEBSITE_DIR,
'destination_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'),
'destination_website_path' => rtrim((string)$update_cfg['repo_root'], '/') . '/Website',
];
$errors = [];
@ -640,17 +673,8 @@ $errors[] = 'Destination Panel path is nested incorrectly: ' . $layout['destinat
if (strpos((string)$layout['destination_website_path'], '/Website/Website') !== false) {
$errors[] = 'Destination Website path is nested incorrectly: ' . $layout['destination_website_path'];
}
if ((realpath(GSP_ROOT_DIR) ?: GSP_ROOT_DIR) !== (realpath(GSP_EXPECTED_ROOT) ?: GSP_EXPECTED_ROOT)) {
$errors[] = 'Live root mismatch. Expected ' . GSP_EXPECTED_ROOT . ' but detected ' . GSP_ROOT_DIR;
}
if ((realpath(GSP_PANEL_DIR) ?: GSP_PANEL_DIR) !== (realpath(GSP_EXPECTED_PANEL) ?: GSP_EXPECTED_PANEL)) {
$errors[] = 'Live Panel mismatch. Expected ' . GSP_EXPECTED_PANEL . ' but detected ' . GSP_PANEL_DIR;
}
if ((realpath(GSP_WEBSITE_DIR) ?: GSP_WEBSITE_DIR) !== (realpath(GSP_EXPECTED_WEBSITE) ?: GSP_EXPECTED_WEBSITE)) {
$errors[] = 'Live Website mismatch. Expected ' . GSP_EXPECTED_WEBSITE . ' but detected ' . GSP_WEBSITE_DIR;
}
if (strpos((realpath($layout['cwd']) ?: $layout['cwd']), (realpath(GSP_PANEL_DIR) ?: GSP_PANEL_DIR)) !== 0) {
$errors[] = 'Updater must run from a working directory under the live Panel path.';
if (!is_dir($layout['destination_panel_path'])) {
$errors[] = 'Destination Panel path does not exist: ' . $layout['destination_panel_path'];
}
gsp_update_log('Deployment layout detection: ' . json_encode($layout));
@ -836,7 +860,7 @@ if ($entry === 'Panel' || $entry === 'Website' || $entry === 'backups' || $entry
continue;
}
$src = rtrim($source_root, '/') . '/' . $entry;
$dst = GSP_ROOT_DIR . '/' . $entry;
$dst = rtrim($layout['live_gsp_root'], '/') . '/' . $entry;
if (is_file($src)) {
$rel = gsp_normalize_rel($entry);
if (gsp_is_preserved_path($rel)) {
@ -852,7 +876,7 @@ $copied_files[] = $rel;
continue;
}
if (is_dir($src)) {
$part = gsp_copy_tree($src, GSP_ROOT_DIR, $entry);
$part = gsp_copy_tree($src, $layout['live_gsp_root'], $entry);
$copied += $part['copied'];
$copied_files = array_merge($copied_files, array_slice((array)$part['copied_files'], 0, max(0, 200 - count($copied_files))));
$skipped = array_merge($skipped, $part['skipped']);
@ -890,7 +914,7 @@ $checks = [
];
foreach ($checks as $rel) {
$src = rtrim($layout['source_repo_root'], '/') . '/' . $rel;
$dst = rtrim(GSP_ROOT_DIR, '/') . '/' . $rel;
$dst = rtrim($layout['live_gsp_root'], '/') . '/' . $rel;
if (!is_file($src)) {
continue;
}
@ -919,10 +943,11 @@ return [
];
}
function gsp_write_last_update_markers()
function gsp_write_last_update_markers($repo_root = null)
{
$line = 'Last Updated at ' . date('g:ia') . ' on ' . date('Y-m-d');
$targets = [GSP_CANONICAL_TIMESTAMP_FILE, GSP_BILLING_TIMESTAMP_FILE];
$last_update_file = rtrim((string)($repo_root ?: GSP_ROOT_DIR), '/') . '/LAST_UPDATE.txt';
$targets = [GSP_CANONICAL_TIMESTAMP_FILE, GSP_BILLING_TIMESTAMP_FILE, $last_update_file];
foreach ($targets as $target) {
$dir = dirname($target);
if (!is_dir($dir)) {
@ -953,15 +978,16 @@ return [
return ['success' => true, 'run' => $run];
}
function gsp_apply_update_from_zip($zip_file, $restart_nonce = '')
function gsp_apply_update_from_zip($zip_file, $restart_nonce = '', array $update_cfg = null)
{
$update_cfg = $update_cfg ?: gsp_update_settings();
$extract = gsp_extract_update_source($zip_file);
if (!$extract['success']) {
return $extract;
}
$temp_dir = $extract['temp_dir'];
$source_root = $extract['source_root'];
$resolved_layout = gsp_resolve_source_layout($temp_dir, $source_root);
$resolved_layout = gsp_resolve_source_layout($temp_dir, $source_root, $update_cfg);
if (!$resolved_layout['success']) {
gsp_rmdir_recursive($temp_dir);
return ['success' => false, 'error' => 'Deployment layout validation failed: ' . implode(' | ', $resolved_layout['errors'])];
@ -985,6 +1011,87 @@ return [
'drift_files' => $drift_files,
];
}
function gsp_checkout_update_source(array $update_cfg)
{
$repo_url = (string)$update_cfg['repo_url'];
$branch = (string)$update_cfg['branch'];
$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.'];
}
$out = [];
$ret = 0;
$cmd = 'git clone --depth 1 --branch ' . escapeshellarg($branch) . ' ' . escapeshellarg($repo_url) . ' ' . escapeshellarg($temp_dir) . ' 2>&1';
exec($cmd, $out, $ret);
if ($ret !== 0) {
gsp_rmdir_recursive($temp_dir);
return ['success' => false, 'error' => 'git clone failed: ' . implode(' | ', array_slice($out, -20))];
}
return ['success' => true, 'temp_dir' => $temp_dir, 'source_root' => $temp_dir, 'output' => implode("\n", $out)];
}
function gsp_apply_update_from_source($source_root, $restart_nonce = '', array $update_cfg = null)
{
$update_cfg = $update_cfg ?: gsp_update_settings();
$resolved_layout = gsp_resolve_source_layout($source_root, $source_root, $update_cfg);
if (!$resolved_layout['success']) {
return ['success' => false, 'error' => 'Deployment layout validation failed: ' . implode(' | ', $resolved_layout['errors'])];
}
$layout = $resolved_layout['layout'];
$_SESSION['gsp_last_update_layout'] = $layout;
$updater_version = substr((string)@hash_file('sha256', $layout['source_panel_path'] . '/modules/administration/panel_update.php'), 0, 12);
$drift_files = gsp_detect_updater_drift_files($layout['source_repo_root'], $layout['live_gsp_root']);
if (!empty($drift_files) && empty($restart_nonce)) {
$copied = gsp_apply_updater_files_only($layout['source_repo_root'], $layout['live_gsp_root'], $drift_files);
$nonce = gsp_random_token(12);
$_SESSION['gsp_update_restart_nonce'] = $nonce;
gsp_update_log('Updater self-update applied (' . $copied . ' files); restart nonce=' . $nonce);
return [
'success' => false,
'restart_required' => true,
'restart_nonce' => $nonce,
'updater_files_updated' => $copied,
'drift_files' => $drift_files,
];
}
if (!empty($restart_nonce)) {
$expected = isset($_SESSION['gsp_update_restart_nonce']) ? $_SESSION['gsp_update_restart_nonce'] : null;
if ($expected === null || !hash_equals($expected, $restart_nonce)) {
return ['success' => false, 'error' => 'Invalid updater restart marker.'];
}
unset($_SESSION['gsp_update_restart_nonce']);
}
$config_file = rtrim($layout['destination_panel_path'], '/') . '/includes/config.inc.php';
$config_backup = is_file($config_file) ? @file_get_contents($config_file) : false;
$patches = gsp_run_required_patches($updater_version);
if (!$patches['success']) {
return ['success' => false, 'error' => $patches['error']];
}
$sync = gsp_apply_layout_sync($layout);
if ($config_backup !== false) {
@file_put_contents($config_file, $config_backup, LOCK_EX);
}
if (!$sync['success']) {
return $sync;
}
$sync_validation = gsp_validate_layout_sync_result($layout, $sync);
if (!$sync_validation['success']) {
return ['success' => false, 'error' => 'Deployed file validation failed: ' . implode(' | ', $sync_validation['errors'])];
}
gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped']));
return [
'success' => true,
'files_copied' => $sync['files_copied'],
'panel_files_copied' => $sync['panel_files_copied'],
'website_files_copied' => $sync['website_files_copied'],
'preserved' => $sync['skipped'],
'copied_files' => $sync['copied_files'],
'patches' => $patches['run'],
];
}
if (!empty($restart_nonce)) {
$expected = isset($_SESSION['gsp_update_restart_nonce']) ? $_SESSION['gsp_update_restart_nonce'] : null;
if ($expected === null || !hash_equals($expected, $restart_nonce)) {
@ -1068,6 +1175,73 @@ if (!$preflight['success']) {
return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])];
}
function gsp_do_configured_git_update(array $update_cfg, $restart_nonce = '')
{
global $db;
$validation = gsp_validate_update_settings($update_cfg);
if (!empty($validation)) {
return ['success' => false, 'error' => implode(' | ', $validation)];
}
$preflight = gsp_preflight_check($update_cfg);
if (!$preflight['success']) {
return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])];
}
$backup = ['success' => true, 'backup_dir' => null];
if (!empty($update_cfg['backup_before_update'])) {
$backup = gsp_create_full_backup('git-update', $update_cfg['branch'], false);
if (!$backup['success']) {
return $backup;
}
gsp_update_log("Backup created before git update to {$update_cfg['branch']}: {$backup['backup_dir']}");
}
$checkout = gsp_checkout_update_source($update_cfg);
if (!$checkout['success']) {
return $checkout;
}
$apply = gsp_apply_update_from_source($checkout['source_root'], $restart_nonce, $update_cfg);
gsp_rmdir_recursive($checkout['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($update_cfg['repo_root']);
gsp_clear_panel_cache($update_cfg['panel_path']);
gsp_write_version_file($update_cfg['branch'], 'git');
gsp_write_version_json('git', $update_cfg['repo_url'], $commit_after ?: $update_cfg['branch'], $commit_after);
gsp_write_last_update_markers();
$db->setSettings(['ogp_version' => $update_cfg['branch'], 'version_type' => 'git']);
if (file_exists($update_cfg['panel_path'] . '/modules/modulemanager/module_handling.php')) {
require_once($update_cfg['panel_path'] . '/modules/modulemanager/module_handling.php');
}
if (function_exists('updateAllPanelModules')) {
updateAllPanelModules();
}
if (function_exists('runPostUpdateOperations')) {
runPostUpdateOperations();
}
gsp_update_log("Configured git update complete: {$update_cfg['repo_url']} {$update_cfg['branch']}");
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'],
];
}
$backup = gsp_create_full_backup($update_type, $ref, false);
if (!$backup['success']) {
return $backup;
@ -1101,7 +1275,7 @@ 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_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')) {
@ -1686,10 +1860,7 @@ if ($_SESSION['users_group'] !== 'admin') {
return;
}
$repo_owner = !empty($settings['gsp_repo_owner']) ? $settings['gsp_repo_owner'] : 'GameServerPanel';
$repo_name = !empty($settings['gsp_repo_name']) ? $settings['gsp_repo_name'] : 'GSP';
$stable_branch = !empty($settings['gsp_stable_branch']) ? $settings['gsp_stable_branch'] : 'Panel-stable';
$unstable_branch = !empty($settings['gsp_unstable_branch']) ? $settings['gsp_unstable_branch'] : 'Panel-unstable';
$update_cfg = gsp_update_settings();
if (empty($_SESSION['gsp_update_csrf'])) {
$_SESSION['gsp_update_csrf'] = gsp_random_token();
@ -1754,6 +1925,58 @@ print_success('Backup created: <code>' . htmlspecialchars($result['backup_dir'])
} else {
print_failure('Backup failed: ' . htmlspecialchars($result['error']));
}
} elseif ($action === 'save_settings') {
$new_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : '',
'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']), '/') : '',
'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
];
$errors = gsp_validate_update_settings($new_cfg);
if (!empty($errors)) {
print_failure('Update settings were not saved: ' . htmlspecialchars(implode(' | ', $errors)));
} else {
$db->setSettings([
'gsp_update_repo_url' => $new_cfg['repo_url'],
'gsp_update_branch' => $new_cfg['branch'],
'gsp_update_repo_root' => $new_cfg['repo_root'],
'gsp_update_panel_path' => $new_cfg['panel_path'],
'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_backup_before'] = $new_cfg['backup_before_update'];
$update_cfg = gsp_update_settings();
print_success('Update settings saved.');
}
} 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'],
'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0',
];
$db->setSettings([
'gsp_update_repo_url' => $update_cfg['repo_url'],
'gsp_update_branch' => $update_cfg['branch'],
'gsp_update_repo_root' => $update_cfg['repo_root'],
'gsp_update_panel_path' => $update_cfg['panel_path'],
'gsp_update_backup_before' => $update_cfg['backup_before_update'],
]);
$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']];
} elseif ($result['success']) {
print_success('Panel updated from configured repository branch <strong>' . htmlspecialchars($update_cfg['branch']) . '</strong>. '
. intval($result['files_copied']) . ' file(s) copied (Panel: ' . intval($result['panel_files_copied']) . ', Website: ' . intval($result['website_files_copied']) . ').');
} else {
print_failure('Update failed: ' . htmlspecialchars($result['error']));
}
} elseif ($action === 'update_release') {
$version = isset($_POST['gsp_release_version']) ? trim($_POST['gsp_release_version']) : '';
if (!preg_match('/^[a-zA-Z0-9._\-]+$/', $version) || strlen($version) > 80) {
@ -1821,17 +2044,129 @@ $last_layout = isset($_SESSION['gsp_last_update_layout']) && is_array($_SESSION[
? $_SESSION['gsp_last_update_layout']
: null;
$vinfo = gsp_read_version_json();
$releases = gsp_fetch_github_releases($repo_owner, $repo_name);
$latest_release = (is_array($releases) && !empty($releases)) ? htmlspecialchars($releases[0]['tag_name']) : 'N/A';
$latest_release = 'N/A';
$backups = gsp_get_available_backups();
$patch_overview = gsp_get_patch_overview();
if ($apache_scan_result === null) {
$apache_scan_result = gsp_scan_apache_configs();
}
if ($preflight_result === null) {
$preflight_result = gsp_preflight_check();
$preflight_result = gsp_preflight_check($update_cfg);
}
$ssl_issue_count = !empty($apache_scan_result['ssl_issues']) ? count($apache_scan_result['ssl_issues']) : 0;
echo "<h2>Panel Updates</h2>\n";
echo "<table class='administration-table'><tr><td>\n";
echo "<h3>Current Installation</h3>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Installed Version:</strong></td><td>" . htmlspecialchars($current_version) . "</td></tr>\n";
echo "<tr><td><strong>Current Branch:</strong></td><td>" . htmlspecialchars($current_branch) . "</td></tr>\n";
if ($git_commit) {
echo "<tr><td><strong>Git Commit:</strong></td><td>" . htmlspecialchars(substr($git_commit, 0, 12)) . "</td></tr>\n";
}
echo "<tr><td><strong>Update Trace Log:</strong></td><td><code>" . htmlspecialchars(GSP_UPDATE_LOG) . "</code></td></tr>\n";
echo "<tr><td><strong>Backups Stored:</strong></td><td>" . intval(count($backups)) . " (retention: 5)</td></tr>\n";
echo "</table>\n";
if ($ssl_issue_count > 0) {
echo "<p style='color:#8a6d3b;'><strong>SSL warning:</strong> " . intval($ssl_issue_count) . " Apache SSL certificate issue(s) detected. Updates are not blocked by this. See Advanced Diagnostics for details.</p>\n";
}
if (!empty($preflight_result['errors'])) {
echo "<p style='color:#a94442;'><strong>Update preflight errors:</strong><br>" . implode('<br>', array_map('htmlspecialchars', $preflight_result['errors'])) . "</p>\n";
}
if (!empty($preflight_result['warnings'])) {
echo "<p style='color:#8a6d3b;'><strong>Update preflight warnings:</strong><br>" . implode('<br>', array_map('htmlspecialchars', $preflight_result['warnings'])) . "</p>\n";
}
echo "<h3>Repository Settings</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Repository URL</strong></td><td><input type='text' name='gsp_update_repo_url' value='" . htmlspecialchars($update_cfg['repo_url'], ENT_QUOTES, 'UTF-8') . "' size='85'></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>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>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 "</table>\n";
echo "<p>\n";
echo "<button type='submit' name='gsp_update_action' value='save_settings'>Save Settings</button> ";
echo "<button type='submit' name='gsp_update_action' value='update_configured' onclick='return confirm(\"Update Panel from the configured repository and branch?\");'>Update Panel</button>\n";
echo "</p>\n";
echo "</form>\n";
echo "<h3>Backup</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='backup_only'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'>Create Backup</button>\n";
echo "</form>\n";
if (!empty($backups)) {
echo "<h3>Rollback</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='revert'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<select name='gsp_revert_backup'>\n";
foreach ($backups as $bk) {
$label = htmlspecialchars($bk['ts']);
if (!empty($bk['meta']['update_target_type']) && !empty($bk['meta']['update_target_version'])) {
$label .= ' (before ' . htmlspecialchars($bk['meta']['update_target_type']) . ': ' . htmlspecialchars($bk['meta']['update_target_version']) . ')';
}
echo "<option value='" . htmlspecialchars($bk['ts']) . "'>{$label}</option>\n";
}
echo "</select> ";
echo "<label><input type='checkbox' name='gsp_restore_apache' value='1'> restore Apache configs if backup contains them</label> ";
echo "<button type='submit' onclick='return confirm(\"Restore Panel, Website, version.json, and database from selected backup?\");'>Rollback</button>\n";
echo "</form>\n";
}
echo "<details style='margin-top:18px;'>\n";
echo "<summary><strong>Advanced Diagnostics</strong></summary>\n";
echo "<h4>Preflight</h4>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Status:</strong></td><td>" . ($preflight_result['success'] ? 'PASS' : 'FAIL') . "</td></tr>\n";
echo "<tr><td><strong>Pending Patches:</strong></td><td>" . intval(count($patch_overview['pending'])) . "</td></tr>\n";
echo "<tr><td><strong>Patch Directory:</strong></td><td><code>" . htmlspecialchars(GSP_PATCH_DIR) . "</code></td></tr>\n";
echo "</table>\n";
echo "<h4>Apache</h4>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Config Directory:</strong></td><td><code>" . htmlspecialchars($apache_scan_result['base']) . "</code></td></tr>\n";
echo "<tr><td><strong>Configs Found:</strong></td><td>" . intval(count($apache_scan_result['files'])) . "</td></tr>\n";
echo "<tr><td><strong>Stale Path Hits:</strong></td><td>" . intval(count($apache_scan_result['stale_issues'])) . "</td></tr>\n";
echo "<tr><td><strong>SSL Certificate Issues:</strong></td><td>" . intval($ssl_issue_count) . "</td></tr>\n";
echo "</table>\n";
if (!empty($apache_scan_result['stale_issues'])) {
echo "<p style='color:#a94442;'><strong>Apache stale path issues:</strong><br>" . implode('<br>', array_map('htmlspecialchars', array_unique($apache_scan_result['stale_issues']))) . "</p>\n";
}
if (!empty($apache_scan_result['ssl_issues'])) {
echo "<p style='color:#8a6d3b;'><strong>SSL certificate issues:</strong><br>";
foreach ((array)$apache_scan_result['ssl_issues'] as $ssl_issue) {
echo htmlspecialchars($ssl_issue['vhost'] . ' ' . $ssl_issue['directive'] . ': ' . $ssl_issue['path'] . ' (' . $ssl_issue['reason'] . ')') . "<br>";
}
echo "</p>\n";
}
echo "<form method='POST' style='display:inline-block;margin-right:8px;'>\n";
echo "<input type='hidden' name='gsp_update_action' value='preflight'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'>Run Preflight Check</button>\n";
echo "</form>\n";
echo "<form method='POST' style='display:inline-block;margin-right:8px;'>\n";
echo "<input type='hidden' name='gsp_update_action' value='apply_patches'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'>Apply Required Patches</button>\n";
echo "</form>\n";
echo "<form method='POST' style='display:inline-block;'>\n";
echo "<input type='hidden' name='gsp_update_action' value='fix_apache'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit' onclick='return confirm(\"Backup Apache configs, run configtest, and apply path fixes?\");'>Fix Apache Paths</button>\n";
echo "</form>\n";
echo "</details>\n";
echo "</td></tr></table>\n";
if ($auto_restart_payload) {
gsp_render_restart_form($auto_restart_payload['action'], $csrf_token, $auto_restart_payload['nonce'], isset($auto_restart_payload['version']) ? $auto_restart_payload['version'] : '');
}
return;
echo "<h2>Panel Updates</h2>\n";
echo "<table class='administration-table'><tr><td>\n";
echo "<h3>Detected Layout</h3>\n";

View file

@ -88,7 +88,32 @@ require_once("modules/config_games/server_config_parser.php");
if($log_retval == 1)
{
$log_url = "home.php?m=gamemanager&p=log&type=cleared&refreshed&home_id-mod_id-ip-port=".rawurlencode($_GET['home_id-mod_id-ip-port']);
echo '<textarea id="live-server-log" class="log" readonly="readonly" style="height:500px;overflow:auto;max-width:1600px;width:100%;box-sizing:border-box;">'.htmlentities($home_log, ENT_QUOTES, "UTF-8").'</textarea>';
echo '<style>
#live-server-log {
display:block;
width:100%;
max-width:1600px;
min-height:55vh;
height:55vh;
box-sizing:border-box;
font-family:Consolas, Monaco, "Courier New", monospace;
font-size:13px;
line-height:1.35;
white-space:pre-wrap;
overflow-y:auto;
overflow-x:auto;
resize:vertical;
tab-size:4;
}
@media (max-width: 700px) {
#live-server-log {
min-height:45vh;
height:45vh;
font-size:12px;
}
}
</style>';
echo '<textarea id="live-server-log" class="log" readonly="readonly" wrap="soft">'.htmlentities($home_log, ENT_QUOTES, "UTF-8").'</textarea>';
?>
<script type="text/javascript">
(function(){

View file

@ -366,6 +366,10 @@ echo "<table id='servermonitor' class='tablesorter' data-sortlist='[[0,0],[3,1]]
$pos,
$ctrlChkBoxes,
$expiration_dates);
$address = "";
$offlineT = "";
$pos = "";
$ctrlChkBoxes = "";
if ( $isAdmin )
$server_home['access_rights'] = $db->getFullAccessRightsString();
@ -487,8 +491,8 @@ echo "<table id='servermonitor' class='tablesorter' data-sortlist='[[0,0],[3,1]]
if($screen_running)
{
// Check if the screen running the server is running.
$status = "online";
$order = 1 + $j;
$status = ($agent_state === "ONLINE") ? "online" : "starting";
$order = ($agent_state === "ONLINE") ? (1 + $j) : (2 + $j);
if($agent_state !== "ONLINE")
{
$address = "<span class='note'>".htmlentities($agent_state)."</span>";
@ -538,6 +542,15 @@ echo "<table id='servermonitor' class='tablesorter' data-sortlist='[[0,0],[3,1]]
}
$stats_servers_online++;
}
elseif($agent_state === "UNKNOWN")
{
$status = "unknown";
$order = 3;
$address = "<span class='note'>Unknown - agent status unavailable.</span>";
if(isset($agent_status['last_error']) && $agent_status['last_error'] !== "")
$address .= " <span class='failure'>".htmlentities($agent_status['last_error'])."</span>";
$offlineT = "<span class='note'>Server state is unknown. The Panel did not confirm that this server is offline.</span>";
}
else
{
$status = "offline";
@ -570,16 +583,20 @@ echo "<table id='servermonitor' class='tablesorter' data-sortlist='[[0,0],[3,1]]
}
}
else{
$status = "offline";
$status = "unknown";
$order = 3;
$address = "<span style='color:darkred;font-weight:bold;'>Agent Offline</span>";
$offlineT = "<span class='note'>Agent is offline. Server state is unknown.</span>";
}
$user = $db->getUserById($server_home['user_id_main']);
// Template
@$first = "<tr class='maintr$trclass'>";
$first .= "<td class='collapsible' data-status='$status' data-pos='$pos'><span class='hidden'>$order</span>" . "<img src='" . check_theme_image("images/$status.png") . "' />" . "</td>";
$status_color = $status === 'online' ? '#2da44e' : ($status === 'starting' ? '#d29922' : ($status === 'unknown' ? '#8b949e' : '#d1242f'));
$status_title = $status === 'starting' ? 'Starting' : ($status === 'unknown' ? 'Unknown' : ucfirst($status));
$status_icon = "<span title='" . htmlentities($status_title) . "' style='display:inline-block;width:14px;height:14px;border-radius:50%;background:" . $status_color . ";border:1px solid rgba(255,255,255,.35);vertical-align:middle;'></span>";
$first .= "<td class='collapsible' data-status='$status' data-pos='$pos'><span class='hidden'>$order</span>" . $status_icon . "</td>";
$first .= "<td class='collapsible'>" . "<span class='hidden'>$mod</span><img src='$icon_path' />" . "</td>";
$first .= "<td class='collapsible serverId hide'>" . $server_home["home_id"] . "</td>";
$first .= "<td class='collapsible' data-status='$status' data-pos='$pos'><b>" . htmlentities($server_home['home_name']) . "</b>$mod_name</td>";

View file

@ -23,6 +23,7 @@ Important references:
- live log retrieval exists
- logs can be fetched through the Panel
- the viewer can update via AJAX
- the main game log viewer uses a large monospace output panel
## What Still Needs Cleanup
@ -31,3 +32,13 @@ Important references:
- better error highlighting
- better downloadable log history
## Log Viewer Layout
`Panel/modules/gamemanager/log.php` keeps the AJAX refresh behavior and renders the log in a large textarea:
- desktop height around `55vh`
- mobile height around `45vh`
- monospace font
- preserved line breaks
- vertical scrolling inside the log panel
- long-line overflow remains usable without shrinking the panel to one line

View file

@ -16,6 +16,19 @@ Important files:
- `Agent_Linux/ogp_agent.pl`
- `Agent-Windows/ogp_agent.pl`
## Panel Display Behavior
Game Monitor should not convert ambiguous status into false offline.
Panel display mapping:
- `ONLINE` displays green.
- `STARTING`, `STOPPING`, and `UNRESPONSIVE` display yellow/active.
- `OFFLINE` displays red only when the agent confirms offline.
- `UNKNOWN` displays gray.
LGSL/GameQ query failure is not enough to mark a server offline. Query success may add player/map/hostname metadata, but query failure should only show a small unavailable note when the agent says the server is otherwise active.
## Recommended State Model
| State | Meaning |
@ -51,4 +64,3 @@ The agent should check, in this order:
- use state hints for start/stop transitions
- expose clear messages for `STARTING` and `UNRESPONSIVE`
- add precise log excerpts when startup fails

View file

@ -62,6 +62,19 @@ Useful state labels:
Query checks should remain optional metadata only.
## Game Monitor Display Rules
`Panel/modules/gamemanager/server_monitor.php` should display state from the agent status response, not from LGSL/GameQ query success alone.
Current display mapping:
- `ONLINE` -> green online indicator
- `STARTING`, `STOPPING`, `UNRESPONSIVE` -> yellow active/starting indicator
- `OFFLINE` -> red offline indicator
- `UNKNOWN` or agent unavailable -> gray unknown indicator
Query metadata remains optional. A running process/session or listening game port must not be shown as red/offline only because query details are unavailable.
## Log Viewer
Relevant files:
@ -72,6 +85,8 @@ Relevant files:
The log view should be treated as live, AJAX-updated output rather than a full page reload workflow.
`log.php` now renders the live log as a large monospace textarea with viewport-based height, preserved line breaks, vertical scrolling, and mobile sizing.
## What This Module Depends On
- `config_games` for startup parameters and protocol definitions
@ -79,4 +94,3 @@ The log view should be treated as live, AJAX-updated output rather than a full p
- `user_games` for server home records
- `rcon` for command support where available
- `addonsmanager` for content/mod interactions

60
docs/modules/UPDATE.md Normal file
View file

@ -0,0 +1,60 @@
# Update Module
## Role
`Panel/modules/update` exposes the admin Panel update page. The page delegates most update behavior to:
- `Panel/modules/administration/panel_update.php`
## Current Behavior
The update page is intentionally simple:
- shows the installed Panel version
- shows the current git branch and commit when available
- exposes editable repository settings
- can create a backup
- can update from the configured repository and branch
- can roll back to an existing backup
- keeps Apache diagnostics in a collapsed Advanced Diagnostics section
## Update Settings
The admin page stores these settings in the Panel settings table:
- `gsp_update_repo_url`
- `gsp_update_branch`
- `gsp_update_repo_root`
- `gsp_update_panel_path`
- `gsp_update_backup_before`
Defaults:
- Repository URL: `http://forge.runlevelsystems.com/dev/GSP.git`
- Branch: `Panel-unstable`
- Repository Root: `/var/www/html/GSP`
- Panel Path: `/var/www/html/GSP/Panel`
- Backup Before Update: enabled
## Update Flow
1. Save or submit repository settings.
2. Validate repository URL, branch, repo root, and Panel path.
3. Run preflight against the configured paths.
4. Create a backup when enabled.
5. Clone the configured repository branch into a temporary checkout.
6. Sync files into the configured root/Panel paths.
7. Preserve `Panel/includes/config.inc.php`.
8. Run module updates/post-update hooks.
9. Write version metadata and `LAST_UPDATE.txt`.
## Diagnostics
Apache and SSL checks are diagnostics only. Missing SSL certificates do not block Panel updates.
The old repeated SSL vhost disable buttons are not part of the primary update page. Apache path repair remains available under Advanced Diagnostics.
## Remaining Issues
- The updater still contains legacy GitHub release helper code that is no longer rendered by the simplified primary UI.
- Real production testing should confirm file ownership and web-server permissions on `/var/www/html/GSP`.