Merge pull request #142 from GameServerPanel/copilot/gsp-automatic-server-install-trigger

Auto-trigger Game Monitor install path on provisioning + monthly-only billing pricing + LiteFM PHP 8.3 hardening
This commit is contained in:
Frank Harris 2026-05-08 17:12:55 -05:00 committed by GitHub
commit e0fdb8cdd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 215 additions and 356 deletions

View file

@ -1,6 +1,8 @@
# Changelog
## 2026-05-08
- **Auto-install trigger + monthly-only billing pricing:** Refactored Game Monitor update/install into a shared callable (`modules/gamemanager/update_actions.php`) reused by billing provisioning so new paid/free/admin-created homes auto-trigger the same install/update path used by `m=gamemanager&p=update&update=refresh` without manual clicks. Billing now treats monthly pricing (`price_monthly`) as canonical across admin service config, add-to-cart, free checkout, PayPal capture, and provisioning end-date math (31-day months), while preserving legacy daily/yearly columns for backward compatibility.
- **LiteFM PHP 8.3 compatibility and install-pending UX:** Removed deprecated `${var}` interpolation usage, guarded missing `fm_cwd_*` session keys and `dirname()` null paths, and replaced directory-not-found warning output with a clear message when server files are not installed yet.
- **Provisioning + billing admin UX reliability pass:** Hardened automatic server provisioning to reserve ports from `arrange_ports` (exact `home_cfg_id`, then `home_cfg_id=0` fallback), prevent duplicate `home_ip_ports` assignment, keep order/invoice/home linkage intact even when install is pending, and apply safe default mod resolution so base installs are not blocked by missing explicit mod choices. Refreshed billing admin service management with row-level save actions, sortable columns (including Game Name enabled-first toggle), clearer save feedback, cleaner unstable-update caution styling, login theme polish, and updated storefront timestamp footer metadata.
- **Steam Workshop reliability + UI simplification:** Removed customer CLI/update scripting instructions from Workshop user flow, reduced per-server behavior options to supported modes only, switched remaining Steam Workshop SQL references to prefix helpers (no `OGP_DB_PREFIX` strings), hardened queued-update agent processing (`queued → updating → installed/failed`) with clearer error persistence, and refreshed monitor/support documentation links to open game-specific docs (fallback to docs index) in a new tab.
- **Billing docs routing refresh:** Updated docs browser links/icons to root-relative storefront paths (`/docs.php`, `/docs/...`) and removed stale hardcoded panel host guidance from getting-started documentation.

View file

@ -9,3 +9,4 @@
- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions.
- Complete a full pass over all `modules/billing/docs/*` game guides to standardize OS/Workshop/RCON capability statements against current XML-backed server support.
- Add an automated billing provisioning integration test fixture that verifies arrange_ports exact/fallback allocation, duplicate-port protection, and home_id linkage after paid/free checkout.
- Add a billing UI badge/filter that distinguishes "pending install" vs "installed" states directly in customer/server order views.

View file

@ -29,19 +29,7 @@ function billing_generate_password(int $bytes = 12): string
function billing_normalize_duration(string $duration): array
{
$duration = strtolower(trim($duration));
switch ($duration) {
case 'day':
case 'daily':
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
case 'year':
case 'yearly':
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
case 'month':
case 'monthly':
default:
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
function billing_money_to_cents(float $amount): int
@ -60,23 +48,17 @@ function billing_rate_from_service(mysqli $db, string $table_prefix, int $servic
return 0.0;
}
$stmt = $db->prepare("SELECT price_daily, price_monthly, price_year FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
$stmt = $db->prepare("SELECT price_monthly FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
if (!$stmt) {
return 0.0;
}
$stmt->bind_param('i', $service_id);
$stmt->execute();
$stmt->bind_result($price_daily, $price_monthly, $price_year);
$stmt->bind_result($price_monthly);
$rate = 0.0;
if ($stmt->fetch()) {
if ($rate_type === 'daily') {
$rate = floatval($price_daily);
} elseif ($rate_type === 'yearly') {
$rate = floatval($price_year);
} else {
$rate = floatval($price_monthly);
}
$rate = floatval($price_monthly);
}
$stmt->close();
@ -179,19 +161,13 @@ $slot_min_qty = 1;
$slot_max_qty = 1;
$durationInfo = billing_normalize_duration($invoice_duration);
if ($service_id > 0) {
$stmt = $db->prepare("SELECT service_name, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
$stmt = $db->prepare("SELECT service_name, price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('i', $service_id);
$stmt->execute();
$stmt->bind_result($service_name, $price_daily, $price_monthly, $price_year, $slot_min_qty, $slot_max_qty);
$stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty);
if ($stmt->fetch()) {
if ($durationInfo['rate_type'] === 'daily') {
$base_rate = floatval($price_daily);
} elseif ($durationInfo['rate_type'] === 'yearly') {
$base_rate = floatval($price_year);
} else {
$base_rate = floatval($price_monthly);
}
$base_rate = floatval($price_monthly);
// constrain slots
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;

View file

@ -21,8 +21,8 @@
.muted { color: #999; font-size: 0.85em; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #155724; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #721c24; }
.servers-cell { text-align: left; }
.server-cb-label { display: block; white-space: nowrap; margin: 2px 0; }
.servers-cell { text-align: left; min-width: 240px; max-width: 280px; }
.server-cb-label { display: block; white-space: normal; margin: 2px 0; }
.action-cell { text-align: center; min-width: 120px; }
.btn-row-save, .btn-save-all {
border: 1px solid #3e7ab8;
@ -65,7 +65,7 @@
*
* Columns that are admin-editable and NEVER overwritten by sync:
* enabled, slot_min_qty, slot_max_qty,
* price_daily, price_monthly, price_year,
* price_monthly,
* remote_server_id, description, img_url
*/
@ -310,7 +310,7 @@ $flashType = 'ok';
$sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game'));
$dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name'));
if (!in_array($sort, ['game', 'config', 'enabled', 'day', 'month', 'year', 'servers'], true)) {
if (!in_array($sort, ['game', 'config', 'enabled', 'month', 'servers'], true)) {
$sort = 'game';
}
if (!in_array($gameMode, ['name', 'enabled'], true)) {
@ -367,9 +367,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
continue;
}
$enabled = isset($svcData['enabled']) ? 1 : 0;
$priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', '');
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', '');
$priceYear = number_format((float)($svcData['price_year'] ?? 0), 2, '.', '');
$slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1));
$slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1));
if ($slotMax < $slotMin) { $slotMax = $slotMin; }
@ -396,9 +394,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
$ok = $db->query(
"UPDATE `{$table_prefix}billing_services`
SET enabled = {$enabled},
price_daily = '{$priceDaily}',
price_monthly = '{$priceMonthly}',
price_year = '{$priceYear}',
slot_min_qty = {$slotMin},
slot_max_qty = {$slotMax},
description = '{$description}',
@ -453,7 +449,7 @@ while ($rsRes && ($row = $rsRes->fetch_assoc())) {
$services = [];
$svcRes = $db->query(
"SELECT bs.service_id, bs.service_name, bs.enabled,
bs.price_daily, bs.price_monthly, bs.price_year,
bs.price_monthly,
bs.slot_min_qty, bs.slot_max_qty,
bs.remote_server_id, bs.description, bs.img_url,
ch.home_cfg_file
@ -474,15 +470,9 @@ if (!empty($services)) {
case 'enabled':
$cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0));
break;
case 'day':
$cmp = ((float)($a['price_daily'] ?? 0)) <=> ((float)($b['price_daily'] ?? 0));
break;
case 'month':
$cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0));
break;
case 'year':
$cmp = ((float)($a['price_year'] ?? 0)) <=> ((float)($b['price_year'] ?? 0));
break;
case 'servers':
$countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen'));
$countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen'));
@ -549,18 +539,10 @@ if (!empty($services)) {
</th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>
<?php $p = sort_link_params('day', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'day' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Day ($)</a>
</th>
<th>
<?php $p = sort_link_params('month', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'month' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Month ($)</a>
</th>
<th>
<?php $p = sort_link_params('year', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'year' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Year ($)</a>
</th>
<th>Description</th>
<th>Image</th>
<th>
@ -615,24 +597,12 @@ if (!empty($services)) {
value="<?php echo (int)$svc['slot_max_qty']; ?>">
</td>
<td>
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_daily]"
value="<?php echo h(number_format((float)$svc['price_daily'], 2, '.', '')); ?>">
</td>
<td>
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_monthly]"
value="<?php echo h(number_format((float)$svc['price_monthly'], 2, '.', '')); ?>">
</td>
<td>
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_year]"
value="<?php echo h(number_format((float)$svc['price_year'], 2, '.', '')); ?>">
</td>
<td>
<input type="text" class="desc-input"
name="svc[<?php echo $sid; ?>][description]"
@ -716,6 +686,7 @@ if (!empty($services)) {
<ul>
<li>A service will only appear in the store when <strong>Enabled</strong> is checked
<em>and</em> at least one server is selected.</li>
<li><strong>Price / Month ($)</strong> is the canonical billing price used by cart, checkout, and provisioning.</li>
<li>The <strong>Game Name</strong> and <strong>Config XML</strong> columns are sourced
from <code><?php echo h("{$table_prefix}config_homes"); ?></code> and are read-only
here. To change them, update the game XML config in the panel.</li>

View file

@ -124,19 +124,7 @@ function cap_invoice_ids_from_custom_id(?string $customId): array {
}
function cap_get_duration_metadata(array $invoice): array {
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
switch ($duration) {
case 'day':
case 'daily':
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
case 'year':
case 'yearly':
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
case 'month':
case 'monthly':
default:
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
function cap_get_end_date(array $invoice, ?string $fromDate = null): string {

View file

@ -111,19 +111,7 @@ require_once __DIR__ . '/classes/BillingService.php';
$repo = new BillingRepository($mysqli, $table_prefix);
$newOrderIds = [];
$duration_meta = static function (array $invoice): array {
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
switch ($duration) {
case 'day':
case 'daily':
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
case 'year':
case 'yearly':
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
case 'month':
case 'monthly':
default:
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
};
foreach ($invoices as $inv) {

View file

@ -33,23 +33,9 @@ class BillingService
{
$qty = max(1, $qty);
$players = max(1, $players);
switch ($rateType) {
case 'daily':
$basePrice = (float)($service['price_daily'] ?? 0);
$periodDays = $qty;
break;
case 'yearly':
$basePrice = (float)($service['price_year'] ?? 0);
$periodDays = $qty * 365;
break;
case 'monthly':
default:
$rateType = 'monthly';
$basePrice = (float)($service['price_monthly'] ?? 0);
$periodDays = $qty * 31;
break;
}
$rateType = 'monthly';
$basePrice = (float)($service['price_monthly'] ?? 0);
$periodDays = $qty * 31;
// price_monthly etc is the per-player per-period rate
$ratePerPlayer = $basePrice;
@ -163,9 +149,7 @@ class BillingService
$periodEnd = $invoiceRow['period_end'] ?? null;
if (!$periodEnd) {
$rateType = $invoiceRow['rate_type'] ?? 'monthly';
$periodMap = ['daily' => '+1 day', 'monthly' => '+31 days', 'yearly' => '+365 days'];
$periodEnd = date('Y-m-d H:i:s', strtotime($periodMap[$rateType] ?? '+31 days'));
$periodEnd = date('Y-m-d H:i:s', strtotime('+31 days'));
}
// If current expiry is in the future, extend from it; otherwise reset from period_end
@ -177,9 +161,7 @@ class BillingService
if ($periodStart && $periodEndVal) {
$currentPeriodSecs = strtotime($periodEndVal) - strtotime($periodStart);
} else {
$rateType2 = $invoiceRow['rate_type'] ?? 'monthly';
$periodSecMap = ['daily' => 86400, 'monthly' => 31 * 86400, 'yearly' => 365 * 86400];
$currentPeriodSecs = $periodSecMap[$rateType2] ?? (31 * 86400);
$currentPeriodSecs = 31 * 86400;
}
$newExpiry = date('Y-m-d H:i:s', strtotime($currentExpiry) + max(86400, $currentPeriodSecs));
} else {

View file

@ -1,6 +1,7 @@
<?php
require_once __DIR__ . '/../../includes/lib_remote.php';
require_once __DIR__ . '/../config_games/server_config_parser.php';
require_once __DIR__ . '/../gamemanager/update_actions.php';
if (!function_exists('billing_generate_provision_password')) {
function billing_generate_provision_password(int $bytes = 12)
@ -389,99 +390,24 @@ function exec_ogp_module()
}
}
//Read the Game Config from the XML file
if (!$order_failed) {
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
if ($server_xml === false) {
$order_failed = true;
$order_failure_reason = "Could not read server XML for home #{$home_id}.";
}
}
//Get Values from XML
$mod_xml = false;
$modkey = '';
$installer_name = '';
if (!$order_failed) {
$selected_mod = $home_info['mods'][$mod_id] ?? reset($home_info['mods']);
if (empty($selected_mod) || empty($selected_mod['mod_key'])) {
$order_failed = true;
$order_failure_reason = "No valid mod profile found for home #{$home_id}.";
} else {
$modkey = (string)$selected_mod['mod_key'];
$mod_xml = xml_get_mod($server_xml, $modkey);
if ($mod_xml === false && isset($server_xml->mods->mod[0])) {
$mod_xml = $server_xml->mods->mod[0];
$modkey = (string)$mod_xml['key'];
}
if ($mod_xml === false) {
$order_failed = true;
$order_failure_reason = "No installable mod profile exists in XML for home #{$home_id}.";
} else {
$installer_name = (string)$mod_xml->installer_name;
$resolved_mod_cfg_id = intval($selected_mod['mod_cfg_id'] ?? $resolved_mod_cfg_id);
}
}
}
//Get Preinstall commands from xml
$precmd = !$order_failed ? $server_xml->pre_install : '';
//Get Postinstall commands from xml
$postcmd = !$order_failed ? $server_xml->post_install : '';
//Enable FTP account in remote server
if (!$order_failed && $ftp == "enabled")
{
$remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']);
$db->changeFtpStatus('enabled',$home_info['home_id']);
}
//Install files for this service in the remote server
if (!$order_failed) {
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
}
if (!$order_failed && (string)$server_xml->installer === "steamcmd" && !empty((string)$installer_name) )
{
if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) )
$cfg_os = "windows";
elseif( preg_match("/linux/", $server_xml->game_key) )
$cfg_os = "linux";
// Some games like L4D2 require anonymous login
if(!empty($mod_xml->installer_login)){
$login = $mod_xml->installer_login;
$pass = '';
}else{
$login = $settings['steam_user'];
$pass = $settings['steam_pass'];
}
$modname = ( $installer_name == '90' and !preg_match("/(cstrike|valve)/", $modkey) ) ? $modkey : '';
$betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : '';
$betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : '';
$arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : '';
$remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname,
$betaname,$betapwd,$login,$pass,$settings['steam_guard'],
$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,'',$arch);
}
elseif (!$order_failed)
{
// No SteamCMD installer — run pre/post install scripts only.
if (!empty((string)$precmd)) {
$result = $remote->exec((string)$precmd);
if ($result === NULL)
$db->logger("Script-only install: pre_install script returned no output for home_id $home_id");
}
if (!empty((string)$postcmd)) {
$result = $remote->exec((string)$postcmd);
if ($result === NULL)
$db->logger("Script-only install: post_install script returned no output for home_id $home_id");
if (!$order_failed) {
$autoInstall = gamemanager_trigger_update_install(
$db,
$home_info,
intval($mod_id),
array('settings' => $settings)
);
$mod_id = intval($autoInstall['mod_id'] ?? $mod_id);
if (empty($autoInstall['ok'])) {
$order_failed = true;
$order_failure_reason = "Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.');
}
}
if (!$order_failed) {
@ -529,54 +455,18 @@ function exec_ogp_module()
}
$end_date_str = date('Y-m-d H:i:s', $existing_end);
}
elseif ($order['invoice_duration'] == "day")
else
{
if(empty($order['end_date']) || $order['end_date'] === NULL){
$end_date = strtotime('+'.$order['qty'].' day');
$qty_days = max(1, intval($order['qty'])) * 31;
if (empty($order['end_date']) || $order['end_date'] === NULL) {
$end_date = strtotime('+' . $qty_days . ' day');
} else {
$current_end = strtotime($order['end_date']);
if ($current_end === false) {
$current_end = time();
}
$end_date = strtotime('+' . $qty_days . ' day', $current_end);
}
else{
//this is a renewel, start from end of previous order
$current_end = strtotime($order['end_date']);
if ($current_end === false) {
$current_end = time(); // fallback to now if date is invalid
}
$end_date = strtotime('+'.$order['qty'].' day', $current_end);
}
}
elseif ($order['invoice_duration'] == "month")
{
// this is a new order
if(empty($order['end_date']) || $order['end_date'] === NULL){
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day');
}
else{
//this is a renewel, start from end of previous order
$current_end = strtotime($order['end_date']);
if ($current_end === false) {
$current_end = time(); // fallback to now if date is invalid
}
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day', $current_end);
}
}
elseif ($order['invoice_duration'] == "year")
{
// this is a new order
if(empty($order['end_date']) || $order['end_date'] === NULL){
$end_date = strtotime('+'.$order['qty'].' year');
}
else{
//this is a renewel, start from end of previous order
$current_end = strtotime($order['end_date']);
if ($current_end === false) {
$current_end = time(); // fallback to now if date is invalid
}
$end_date = strtotime('+'.$order['qty'].' year', $current_end);
}
}
if (!isset($end_date_str)) {
$end_date_str = date('Y-m-d H:i:s', $end_date);

View file

@ -176,7 +176,7 @@ if ($db && $user_id > 0) {
<h3>What Happens Next?</h3>
<ul>
<li><strong> Payment Confirmed:</strong> Your payment has been captured by PayPal</li>
<li><strong>⚙️ Server Provisioning:</strong> Your game server(s) will be automatically created when you log into the panel</li>
<li><strong>⚙️ Server Provisioning:</strong> Your game server(s) are queued for automatic install now; if a node is unavailable they remain clearly marked as pending install</li>
<li><strong>📧 Email Notification:</strong> You'll receive a confirmation email with your order details</li>
<li><strong>🎮 Access Your Servers:</strong> Log into the Game Server Panel to manage your new servers</li>
</ul>

View file

@ -1 +1 @@
Last Updated at 9:18pm on 2026-08-05
Last Updated at 10:02pm on 2026-05-08

View file

@ -0,0 +1,130 @@
<?php
if (!function_exists('gamemanager_choose_mod_id')) {
function gamemanager_choose_mod_id(array $home_info, int $preferred_mod_id = 0): int
{
$mods = $home_info['mods'] ?? array();
if (!is_array($mods) || empty($mods)) {
return 0;
}
if ($preferred_mod_id > 0 && isset($mods[$preferred_mod_id])) {
return $preferred_mod_id;
}
$keys = array_keys($mods);
return intval(reset($keys));
}
}
if (!function_exists('gamemanager_trigger_update_install')) {
function gamemanager_trigger_update_install($db, array $home_info, int $mod_id, array $options = array()): array
{
$home_id = intval($home_info['home_id'] ?? 0);
$mod_id = gamemanager_choose_mod_id($home_info, $mod_id);
if ($home_id <= 0) {
return array('ok' => false, 'pending' => true, 'message' => 'Invalid home_id.', 'mod_id' => $mod_id);
}
if ($mod_id <= 0 || empty($home_info['mods'][$mod_id])) {
return array('ok' => false, 'pending' => true, 'message' => "No mod profile configured for home #{$home_id}.", 'mod_id' => $mod_id);
}
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if (!$server_xml) {
return array('ok' => false, 'pending' => true, 'message' => "Could not read server config XML for home #{$home_id}.", 'mod_id' => $mod_id);
}
$remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']);
if ($remote->status_chk() === 0) {
return array('ok' => false, 'pending' => true, 'message' => 'Agent is offline.', 'mod_id' => $mod_id);
}
if ($remote->is_screen_running(OGP_SCREEN_TYPE_HOME, $home_id) == 1) {
return array('ok' => false, 'pending' => false, 'message' => 'Server is running and cannot be updated.', 'mod_id' => $mod_id);
}
$log_txt = '';
$update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, $home_id, clean_path($home_info['home_path']), $log_txt);
if ($update_active == 1) {
return array('ok' => true, 'started' => true, 'already_running' => true, 'message' => 'Update already in progress.', 'mod_id' => $mod_id);
}
$modkey = $home_info['mods'][$mod_id]['mod_key'] ?? '';
$mod_xml = xml_get_mod($server_xml, $modkey);
if (!$mod_xml) {
return array('ok' => false, 'pending' => true, 'message' => "Mod key '{$modkey}' not found in XML.", 'mod_id' => $mod_id);
}
$installer_name = isset($mod_xml->installer_name) ? (string)$mod_xml->installer_name : (string)$modkey;
$precmd = $home_info['mods'][$mod_id]['precmd'] == ""
? ($home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install : $home_info['mods'][$mod_id]['def_precmd'])
: $home_info['mods'][$mod_id]['precmd'];
$postcmd = $home_info['mods'][$mod_id]['postcmd'] == ""
? ($home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : $home_info['mods'][$mod_id]['def_postcmd'])
: $home_info['mods'][$mod_id]['postcmd'];
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location);
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name);
$master_server_home_id = intval($options['master_server_home_id'] ?? 0);
if ($master_server_home_id > 0) {
if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) != $master_server_home_id) {
return array('ok' => false, 'pending' => false, 'message' => 'Attempting update from non-master server.', 'mod_id' => $mod_id);
}
if ($master_server_home_id == $home_id) {
return array('ok' => false, 'pending' => false, 'message' => 'Cannot update from own self.', 'mod_id' => $mod_id);
}
$ms_info = $db->getGameHome($master_server_home_id);
$steam_out = $remote->masterServerUpdate($home_id, $home_info['home_path'], $master_server_home_id, $ms_info['home_path'], $exec_folder_path, $exec_path, $precmd, $postcmd);
if ($steam_out === 0) {
return array('ok' => false, 'pending' => true, 'message' => 'Failed to start master update.', 'mod_id' => $mod_id);
}
return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id);
}
$use_steamcmd = ((string)$server_xml->installer === "steamcmd");
if ($use_steamcmd && !empty((string)$installer_name)) {
$cfg_os = '';
if (preg_match("/win32/", $server_xml->game_key) || preg_match("/win64/", $server_xml->game_key)) {
$cfg_os = "windows";
} elseif (preg_match("/linux/", $server_xml->game_key)) {
$cfg_os = "linux";
}
$settings = is_array($options['settings'] ?? null) ? $options['settings'] : $db->getSettings();
if (!empty($mod_xml->installer_login)) {
$login = (string)$mod_xml->installer_login;
$pass = '';
} else {
$login = (string)($settings['steam_user'] ?? '');
$pass = (string)($settings['steam_pass'] ?? '');
}
$modname = ($installer_name == '90') ? $modkey : '';
$betaname = isset($mod_xml->betaname) ? (string)$mod_xml->betaname : '';
$betapwd = isset($mod_xml->betapwd) ? (string)$mod_xml->betapwd : '';
$arch = isset($mod_xml->steam_bitness) ? (string)$mod_xml->steam_bitness : '';
$lockFiles = (isset($server_xml->lock_files) && !empty($server_xml->lock_files)) ? trim((string)$server_xml->lock_files) : "";
$steam_out = $remote->steam_cmd($home_id, $home_info['home_path'], $installer_name, $modname,
$betaname, $betapwd, $login, $pass, $settings['steam_guard'] ?? '',
$exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $lockFiles, $arch);
if ($steam_out === 0) {
return array('ok' => false, 'pending' => true, 'message' => 'Failed to start SteamCMD update.', 'mod_id' => $mod_id);
}
return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id);
}
$ran_scripts = false;
if (!empty((string)$precmd)) {
$remote->exec((string)$precmd);
$ran_scripts = true;
}
if (!empty((string)$postcmd)) {
$remote->exec((string)$postcmd);
$ran_scripts = true;
}
return array(
'ok' => true,
'started' => $ran_scripts,
'completed' => !$ran_scripts,
'message' => $ran_scripts ? 'Script install started.' : 'No installer command was required.',
'mod_id' => $mod_id
);
}
}

View file

@ -24,6 +24,7 @@
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once("modules/gamemanager/update_actions.php");
function exec_ogp_module() {
@ -90,111 +91,27 @@ function exec_ogp_module() {
// Start update.
else if ($_GET['update'] == 'update' && $update_active != 1)
{
$installer_name = $modkey;
if ( isset( $mod_xml->installer_name ) )
{
$installer_name = $mod_xml->installer_name;
$start_result = gamemanager_trigger_update_install(
$db,
$home_info,
intval($mod_id),
array(
'master_server_home_id' => isset($_REQUEST['master_server_home_id']) ? intval($_REQUEST['master_server_home_id']) : 0,
'settings' => $db->getSettings(),
)
);
$mod_id = intval($start_result['mod_id'] ?? $mod_id);
if (empty($start_result['ok'])) {
print_failure(!empty($start_result['message']) ? $start_result['message'] : get_lang("failed_to_start_steam_update"));
return;
}
$precmd = $home_info['mods'][$mod_id]['precmd'] == "" ?
( $home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install :
$home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['precmd'];
$postcmd = $home_info['mods'][$mod_id]['postcmd'] == "" ?
( $home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install :
$home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['postcmd'];
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
if( isset( $_REQUEST['master_server_home_id'] ) )
{
$ms_home_id = $_REQUEST['master_server_home_id'];
if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) == $ms_home_id) {
if ($ms_home_id !== $home_id) {
$ms_info = $db->getGameHome($ms_home_id);
$steam_out = $remote->masterServerUpdate( $home_id,$home_info['home_path'],$ms_home_id,$ms_info['home_path'],$exec_folder_path,$exec_path,$precmd,$postcmd );
} else {
print_failure(get_lang('cannot_update_from_own_self'));
$view->refresh('?m=gamemanager&p=game_monitor', 2);
return;
}
} else {
$db->logger(get_lang_f('update_attempt_from_nonmaster_server', $_SESSION['users_login'], $home_id, $ms_home_id));
print_failure(get_lang('attempting_nonmaster_update'));
$view->refresh('?m=gamemanager&p=game_monitor', 2);
return;
}
}
elseif ($use_steamcmd && !empty((string)$installer_name))
{
if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) )
$cfg_os = "windows";
elseif( preg_match("/linux/", $server_xml->game_key) )
$cfg_os = "linux";
$settings = $db->getSettings();
// Some games like L4D2 require anonymous login
if($mod_xml->installer_login){
$login = $mod_xml->installer_login;
$pass = '';
}else{
$login = $settings['steam_user'];
$pass = $settings['steam_pass'];
}
$modname = ( $installer_name == '90' ) ? $modkey : '';
$betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : '';
$betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : '';
$arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : '';
// Additional files to lock
if(isset($server_xml->lock_files) && !empty($server_xml->lock_files)){
$lockFiles = trim($server_xml->lock_files);
}else{
$lockFiles = "";
}
$steam_out = $remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname,
$betaname,$betapwd,$login,$pass,$settings['steam_guard'],
$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,$lockFiles,$arch);
}
else
{
// No SteamCMD installer — run pre/post install scripts only.
$ran_scripts = false;
if (!empty((string)$precmd)) {
$remote->exec((string)$precmd);
$ran_scripts = true;
}
if (!empty((string)$postcmd)) {
$remote->exec((string)$postcmd);
$ran_scripts = true;
}
if ($ran_scripts) {
print_success( get_lang("update_started") );
} else {
print_success( get_lang("update_completed") );
}
if (!empty($start_result['started'])) {
print_success(get_lang("update_started"));
} else {
print_success(get_lang("update_completed"));
$view->refresh("?m=gamemanager&amp;p=game_monitor&amp;home_id=$home_id", 3);
return;
}
if( $steam_out === 0 )
{
print_failure( get_lang("failed_to_start_steam_update") );
return;
}
else if ( $steam_out === 1 )
{
print_success( get_lang("update_started") );
}
}
// Refresh update page.
else

View file

@ -64,16 +64,30 @@ function exec_ogp_module()
// We must always add the home directory to the fm_cwd so that user
// can not go out of the homedir.
$path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]);
$cwd_session_key = 'fm_cwd_' . $home_id;
if (!isset($_SESSION[$cwd_session_key]) || !is_string($_SESSION[$cwd_session_key])) {
$_SESSION[$cwd_session_key] = '';
}
$path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]);
if (!$remote->rfile_exists($path))
{
while(!$remote->rfile_exists($path))
{
$_SESSION['fm_cwd_'.$home_id] = dirname($_SESSION['fm_cwd_'.$home_id]);
$path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]);
$current_cwd = isset($_SESSION[$cwd_session_key]) ? (string)$_SESSION[$cwd_session_key] : '';
if ($current_cwd === '' || $current_cwd === '.' || $current_cwd === DIRECTORY_SEPARATOR) {
print_failure("Server files have not been installed yet.");
echo "<table class='center'><tr><td><a href='?m=gamemanager&amp;p=game_monitor&amp;home_id=".$home_cfg['home_id']."'><< ".get_lang('back')."</a></td></tr></table>";
return;
}
$parent_cwd = dirname($current_cwd);
if (!is_string($parent_cwd) || $parent_cwd === '.' || $parent_cwd === DIRECTORY_SEPARATOR) {
$parent_cwd = '';
}
$_SESSION[$cwd_session_key] = $parent_cwd;
$path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]);
if($path == clean_path($home_cfg['home_path']."/"))
{
print_failure(get_lang_f("dir_not_found",$path));
print_failure("Server files have not been installed yet.");
echo "<table class='center'><tr><td><a href='?m=gamemanager&amp;p=game_monitor&amp;home_id=".$home_cfg['home_id']."'><< ".get_lang('back')."</a></td></tr></table>";
return;
}
@ -214,7 +228,7 @@ function exec_ogp_module()
{
$remote->shell_action('remove_recursive', $files);
$files = str_replace('" "','"<br>"',$files);
$db->logger( get_lang("remove") . ": ${files}" );
$db->logger( get_lang("remove") . ": {$files}" );
}
}
}
@ -349,7 +363,7 @@ function exec_ogp_module()
if($items != '')
{
$retval = $remote->compress_files($items,$path,$archive_name,$archive_type);
$archive = clean_path( "${path}/${archive_name}.${archive_type}" );
$archive = clean_path( "{$path}/{$archive_name}.{$archive_type}" );
if( $retval == 0 )
{
do{