Merge pull request #163 from GameServerPanel/copilot/fix-gsp-updater-deployment-layout
This commit is contained in:
commit
faf2dd6bf8
3 changed files with 198 additions and 21 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-19
|
||||
- **Updater deployment layout enforcement + marker file writes:** Hardened `modules/administration/panel_update.php` to require the live `/var/www/html/GSP` root with explicit `/Panel` and `/Website` destinations, log full source/destination path detection (including temp checkout/source roots), stop on invalid mapping, copy Panel/Website via explicit subtree mapping (preventing nested `Panel/Panel` or `Website/Website`), validate key copied files after sync, and write both `Website/timestamp.txt` and `modules/billing/timestamp.txt` marker files on successful updates.
|
||||
- **Workshop content script path fix:** Updated addonsmanager default Workshop script paths to the panel module script location under `/var/www/html/GSP/Panel/modules/addonsmanager/scripts/workshop/` so Workshop actions no longer use the incorrect game-home-mixed path on the agent host.
|
||||
|
||||
## 2026-05-18
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@
|
|||
- Add Phase 2 Workshop Content UX in `addonsmanager`: browse/search/select Workshop items with metadata while reusing the Phase 1 per-home saved-ID action pipeline.
|
||||
- Add localized language strings/tooltips for the new cron scheduler `server_content_*` action labels across all supported panel locales.
|
||||
- Add a Game Manager "Live Server Status" panel that consumes `Panel/protocol/gsp_query.php` and shows banner preview plus copyable embed code.
|
||||
- Add an updater admin UI table that renders the full deployment preflight path map (temp checkout, source repo/panel/website, destination panel/website) directly from the new layout detection payload for one-click operator verification.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ defined('GSP_VERSION_FILE') || define('GSP_VERSION_FILE', GSP_PANEL_DIR . '/incl
|
|||
defined('GSP_VERSION_JSON') || define('GSP_VERSION_JSON', GSP_ROOT_DIR . '/version.json');
|
||||
defined('GSP_PATCH_DIR') || define('GSP_PATCH_DIR', GSP_PANEL_DIR . '/modules/update/patches');
|
||||
defined('GSP_EXPECTED_ROOT') || define('GSP_EXPECTED_ROOT', '/var/www/html/GSP');
|
||||
defined('GSP_EXPECTED_PANEL') || define('GSP_EXPECTED_PANEL', GSP_EXPECTED_ROOT . '/Panel');
|
||||
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');
|
||||
|
||||
$gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php';
|
||||
if (file_exists($gspPatchManager)) {
|
||||
|
|
@ -207,33 +211,55 @@ function gsp_preflight_check()
|
|||
{
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
$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;
|
||||
$layout = [
|
||||
'cwd' => getcwd(),
|
||||
'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,
|
||||
'gsp_root_real' => $root_real,
|
||||
'panel_dir' => GSP_PANEL_DIR,
|
||||
'panel_dir_real' => $panel_real,
|
||||
'website_dir' => GSP_WEBSITE_DIR,
|
||||
'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,
|
||||
];
|
||||
|
||||
if (!$layout['cwd']) {
|
||||
$errors[] = 'Unable to read current working directory.';
|
||||
} elseif (strpos(realpath($layout['cwd']) ?: $layout['cwd'], GSP_ROOT_DIR) !== 0) {
|
||||
$errors[] = 'Current working directory is outside detected GSP root.';
|
||||
} elseif (strpos($cwd_real, $panel_real) !== 0) {
|
||||
$errors[] = 'Current working directory must be under live Panel path: ' . $panel_real;
|
||||
}
|
||||
if (!is_dir(GSP_ROOT_DIR)) {
|
||||
$errors[] = 'Detected GSP root path is missing.';
|
||||
}
|
||||
if (realpath(GSP_ROOT_DIR) !== false && realpath(GSP_ROOT_DIR) !== GSP_EXPECTED_ROOT) {
|
||||
$warnings[] = 'Detected root differs from expected production path: ' . GSP_EXPECTED_ROOT;
|
||||
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)) {
|
||||
$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 (!file_exists($layout['config_file'])) {
|
||||
$errors[] = 'Panel includes/config.inc.php was not found and cannot be preserved.';
|
||||
}
|
||||
|
|
@ -567,6 +593,78 @@ $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)
|
||||
{
|
||||
$source_root_real = realpath($source_root) ?: $source_root;
|
||||
$candidates = [$source_root_real];
|
||||
if (basename($source_root_real) === 'Panel' || basename($source_root_real) === 'Website') {
|
||||
$candidates[] = dirname($source_root_real);
|
||||
}
|
||||
$candidates = array_values(array_unique(array_filter($candidates)));
|
||||
$repo_root = null;
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_dir($candidate . '/Panel') && is_dir($candidate . '/Website')) {
|
||||
$repo_root = realpath($candidate) ?: $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$layout = [
|
||||
'cwd' => getcwd() ?: '',
|
||||
'live_gsp_root' => GSP_ROOT_DIR,
|
||||
'live_panel_path' => GSP_PANEL_DIR,
|
||||
'live_website_path' => GSP_WEBSITE_DIR,
|
||||
'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,
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
if (!$repo_root) {
|
||||
$errors[] = 'Unable to resolve source repository root containing both Panel/ and Website/.';
|
||||
} else {
|
||||
if (!is_dir($layout['source_panel_path'])) {
|
||||
$errors[] = 'Source Panel path is missing: ' . $layout['source_panel_path'];
|
||||
}
|
||||
if (!is_dir($layout['source_website_path'])) {
|
||||
$errors[] = 'Source Website path is missing: ' . $layout['source_website_path'];
|
||||
}
|
||||
}
|
||||
if (strpos((string)$layout['destination_panel_path'], '/Panel/Panel') !== false) {
|
||||
$errors[] = 'Destination Panel path is nested incorrectly: ' . $layout['destination_panel_path'];
|
||||
}
|
||||
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.';
|
||||
}
|
||||
|
||||
gsp_update_log('Deployment layout detection: ' . json_encode($layout));
|
||||
foreach ($errors as $error) {
|
||||
gsp_update_log('Deployment layout error: ' . $error);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'errors' => $errors,
|
||||
'layout' => $layout,
|
||||
];
|
||||
}
|
||||
|
||||
function gsp_normalize_rel($path)
|
||||
{
|
||||
$path = str_replace('\\', '/', $path);
|
||||
|
|
@ -715,8 +813,13 @@ $copied++;
|
|||
return $copied;
|
||||
}
|
||||
|
||||
function gsp_apply_layout_sync($source_root)
|
||||
function gsp_apply_layout_sync(array $layout)
|
||||
{
|
||||
$source_root = $layout['source_repo_root'];
|
||||
$source_panel = $layout['source_panel_path'];
|
||||
$source_website = $layout['source_website_path'];
|
||||
$destination_panel = $layout['destination_panel_path'];
|
||||
$destination_website = $layout['destination_website_path'];
|
||||
$top_level = scandir($source_root);
|
||||
$skip = ['.', '..', '.git', '.github', '.gitignore', '.vscode'];
|
||||
$copied = 0;
|
||||
|
|
@ -724,17 +827,16 @@ $panel_copied = 0;
|
|||
$website_copied = 0;
|
||||
$skipped = [];
|
||||
$copied_files = [];
|
||||
gsp_update_log('Layout sync source mapping: ' . $source_root . '/Panel => ' . GSP_PANEL_DIR . ' ; ' . $source_root . '/Website => ' . GSP_WEBSITE_DIR);
|
||||
gsp_update_log('Layout sync source mapping: ' . $source_panel . ' => ' . $destination_panel . ' ; ' . $source_website . ' => ' . $destination_website);
|
||||
foreach ((array)$top_level as $entry) {
|
||||
if (in_array($entry, $skip, true)) {
|
||||
continue;
|
||||
}
|
||||
$src = rtrim($source_root, '/') . '/' . $entry;
|
||||
$dst = GSP_ROOT_DIR . '/' . $entry;
|
||||
if ($entry === 'backups') {
|
||||
$skipped[] = 'backups/';
|
||||
if ($entry === 'Panel' || $entry === 'Website' || $entry === 'backups' || $entry === 'logs') {
|
||||
continue;
|
||||
}
|
||||
$src = rtrim($source_root, '/') . '/' . $entry;
|
||||
$dst = GSP_ROOT_DIR . '/' . $entry;
|
||||
if (is_file($src)) {
|
||||
$rel = gsp_normalize_rel($entry);
|
||||
if (gsp_is_preserved_path($rel)) {
|
||||
|
|
@ -753,15 +855,21 @@ if (is_dir($src)) {
|
|||
$part = gsp_copy_tree($src, GSP_ROOT_DIR, $entry);
|
||||
$copied += $part['copied'];
|
||||
$copied_files = array_merge($copied_files, array_slice((array)$part['copied_files'], 0, max(0, 200 - count($copied_files))));
|
||||
if ($entry === 'Panel') {
|
||||
$panel_copied += $part['copied'];
|
||||
}
|
||||
if ($entry === 'Website') {
|
||||
$website_copied += $part['copied'];
|
||||
}
|
||||
$skipped = array_merge($skipped, $part['skipped']);
|
||||
}
|
||||
}
|
||||
$panel_part = gsp_copy_tree($source_panel, dirname($destination_panel), basename($destination_panel));
|
||||
$copied += $panel_part['copied'];
|
||||
$panel_copied += $panel_part['copied'];
|
||||
$copied_files = array_merge($copied_files, array_slice((array)$panel_part['copied_files'], 0, max(0, 200 - count($copied_files))));
|
||||
$skipped = array_merge($skipped, $panel_part['skipped']);
|
||||
|
||||
$website_part = gsp_copy_tree($source_website, dirname($destination_website), basename($destination_website));
|
||||
$copied += $website_part['copied'];
|
||||
$website_copied += $website_part['copied'];
|
||||
$copied_files = array_merge($copied_files, array_slice((array)$website_part['copied_files'], 0, max(0, 200 - count($copied_files))));
|
||||
$skipped = array_merge($skipped, $website_part['skipped']);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'files_copied' => $copied,
|
||||
|
|
@ -772,6 +880,62 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
function gsp_validate_layout_sync_result(array $layout, array $sync)
|
||||
{
|
||||
$errors = [];
|
||||
$checks = [
|
||||
'Panel/modules/administration/panel_update.php',
|
||||
'Panel/modules/addonsmanager/addons_manager.php',
|
||||
'Website/index.php',
|
||||
];
|
||||
foreach ($checks as $rel) {
|
||||
$src = rtrim($layout['source_repo_root'], '/') . '/' . $rel;
|
||||
$dst = rtrim(GSP_ROOT_DIR, '/') . '/' . $rel;
|
||||
if (!is_file($src)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_file($dst)) {
|
||||
$errors[] = 'Missing deployed file: ' . $rel;
|
||||
continue;
|
||||
}
|
||||
$src_hash = @hash_file('sha256', $src);
|
||||
$dst_hash = @hash_file('sha256', $dst);
|
||||
if ($src_hash === false || $dst_hash === false || $src_hash !== $dst_hash) {
|
||||
$errors[] = 'Copied file verification failed: ' . $rel;
|
||||
}
|
||||
}
|
||||
if (!empty($sync['copied_files']) && intval($sync['panel_files_copied']) === 0) {
|
||||
$errors[] = 'No Panel files were copied during layout sync.';
|
||||
}
|
||||
if (!empty($sync['copied_files']) && intval($sync['website_files_copied']) === 0) {
|
||||
$errors[] = 'No Website files were copied during layout sync.';
|
||||
}
|
||||
foreach ($errors as $error) {
|
||||
gsp_update_log('Layout sync validation error: ' . $error);
|
||||
}
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
function gsp_write_last_update_markers()
|
||||
{
|
||||
$line = 'Last Updated at ' . date('g:ia') . ' on ' . date('Y-m-d');
|
||||
$targets = [GSP_CANONICAL_TIMESTAMP_FILE, GSP_BILLING_TIMESTAMP_FILE];
|
||||
foreach ($targets as $target) {
|
||||
$dir = dirname($target);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
if (is_writable($dir) || is_writable($target)) {
|
||||
@file_put_contents($target, $line . PHP_EOL, LOCK_EX);
|
||||
}
|
||||
}
|
||||
gsp_update_log('Last-update marker files written: ' . implode(', ', $targets));
|
||||
return $line;
|
||||
}
|
||||
|
||||
function gsp_run_required_patches($updater_version)
|
||||
{
|
||||
global $db;
|
||||
|
|
@ -797,11 +961,17 @@ return $extract;
|
|||
}
|
||||
$temp_dir = $extract['temp_dir'];
|
||||
$source_root = $extract['source_root'];
|
||||
$updater_version = substr((string)@hash_file('sha256', $source_root . '/Panel/modules/administration/panel_update.php'), 0, 12);
|
||||
$resolved_layout = gsp_resolve_source_layout($temp_dir, $source_root);
|
||||
if (!$resolved_layout['success']) {
|
||||
gsp_rmdir_recursive($temp_dir);
|
||||
return ['success' => false, 'error' => 'Deployment layout validation failed: ' . implode(' | ', $resolved_layout['errors'])];
|
||||
}
|
||||
$layout = $resolved_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($source_root, GSP_ROOT_DIR);
|
||||
$drift_files = gsp_detect_updater_drift_files($layout['source_repo_root'], GSP_ROOT_DIR);
|
||||
if (!empty($drift_files) && empty($restart_nonce)) {
|
||||
$copied = gsp_apply_updater_files_only($source_root, GSP_ROOT_DIR, $drift_files);
|
||||
$copied = gsp_apply_updater_files_only($layout['source_repo_root'], GSP_ROOT_DIR, $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);
|
||||
|
|
@ -829,11 +999,15 @@ gsp_rmdir_recursive($temp_dir);
|
|||
return ['success' => false, 'error' => $patches['error']];
|
||||
}
|
||||
|
||||
$sync = gsp_apply_layout_sync($source_root);
|
||||
$sync = gsp_apply_layout_sync($layout);
|
||||
gsp_rmdir_recursive($temp_dir);
|
||||
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']));
|
||||
gsp_update_log('Layout sync totals: Panel=' . intval($sync['panel_files_copied']) . ', Website=' . intval($sync['website_files_copied']));
|
||||
if (!empty($sync['skipped'])) {
|
||||
|
|
@ -926,6 +1100,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();
|
||||
$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]);
|
||||
|
||||
if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue