server content features improved

This commit is contained in:
Frank Harris 2026-06-18 10:04:36 -05:00
parent a28d3e1a4f
commit 8a56ddc83c
7 changed files with 464 additions and 3 deletions

View file

@ -11,14 +11,16 @@ $(function() {
download_zip: ['#scm-row-url', '#scm-row-path'],
steam_workshop: ['#scm-row-workshop-xml-info'],
post_script: ['#scm-row-post-script'],
config_edit: ['#scm-row-path', '#scm-row-config-edit-rule']
config_edit: ['#scm-row-path', '#scm-row-config-edit-rule'],
server_app: ['.scm-row-server-app']
};
var allRows = [
'#scm-row-url',
'#scm-row-path',
'#scm-row-workshop-xml-info',
'#scm-row-post-script',
'#scm-row-config-edit-rule'
'#scm-row-config-edit-rule',
'.scm-row-server-app'
];
var $method = $('#scm-install-method');
var $help = $('#scm-install-method-help');

View file

@ -171,7 +171,7 @@ function exec_ogp_module() {
{
$addon_id = (int)$_REQUEST['addon_id'];
$addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
$addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name, hook_name, hook_enabled, hook_platform, hook_working_dir, hook_start_command, hook_stop_command, hook_start_timing, hook_stop_with_server, hook_watch, hook_critical, hook_kill_game_if_app_exits, hook_restart_app_if_exits, hook_pid_name, hook_app_name, hook_description FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
if (!is_array($addons_rows)) {
$addons_rows = [];
}
@ -183,6 +183,15 @@ function exec_ogp_module() {
}
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
if ($state == "start") {
$gsp_content_base = rtrim((string)$home_info['home_path'], '/\\') . '/_gsp_content';
$remote->exec(
'mkdir -p ' .
scm_remote_shell_quote($gsp_content_base . '/hooks') . ' ' .
scm_remote_shell_quote($gsp_content_base . '/generated') . ' ' .
scm_remote_shell_quote($gsp_content_base . '/runtime')
);
}
$addon_info = $addons_rows[0];
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
@ -393,6 +402,44 @@ function exec_ogp_module() {
echo "<p><a href=\"?m=addonsmanager&amp;p=user_addons&amp;home_id=$home_id&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port\">".get_lang('back')."</a></p>";
return;
}
if ($install_method === 'server_app') {
$hook_message = '';
$hook_ok = scm_install_server_app_hook($remote, $home_info, $addon_info, $hook_message);
if ($hook_ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, 'hook_manifest=' . $hook_message);
scm_upsert_manifest($db, $home_id, $addon_id, array(
'install_method' => $install_method,
'content_version' => $content_version,
'install_state' => 'installed',
'source_url' => '',
'installed_by' => $user_id,
));
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'target_path' => $hook_message,
'action' => 'succeeded',
));
print_success('Server-side application hook installed.');
} else {
scm_record_install_done($db, (int)$history_id, 'failed', 1, $hook_message);
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'action' => 'failed',
'error' => $hook_message,
));
print_failure($hook_message);
}
echo "<p><a href=\"?m=addonsmanager&amp;p=user_addons&amp;home_id=$home_id&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port\">".get_lang('back')."</a></p>";
return;
}
if ($install_method === 'config_edit' || $install_method === 'create_folder') {
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location));
$target_template = trim((string)$addon_info['path']);

View file

@ -72,6 +72,21 @@ function exec_ogp_module() {
$fields['optional_folder_name']= '';
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
$fields['launch_param_additions'] = '';
$fields['hook_name'] = isset($_POST['hook_name']) ? trim((string)$_POST['hook_name']) : '';
$fields['hook_enabled'] = !empty($_POST['hook_enabled']) ? 1 : 0;
$fields['hook_platform'] = scm_normalize_hook_platform(isset($_POST['hook_platform']) ? $_POST['hook_platform'] : 'both');
$fields['hook_working_dir'] = isset($_POST['hook_working_dir']) ? trim((string)$_POST['hook_working_dir']) : '';
$fields['hook_start_command'] = isset($_POST['hook_start_command']) ? trim((string)$_POST['hook_start_command']) : '';
$fields['hook_stop_command'] = isset($_POST['hook_stop_command']) ? trim((string)$_POST['hook_stop_command']) : '';
$fields['hook_start_timing'] = scm_normalize_hook_start_timing(isset($_POST['hook_start_timing']) ? $_POST['hook_start_timing'] : 'before_server');
$fields['hook_stop_with_server'] = !empty($_POST['hook_stop_with_server']) ? 1 : 0;
$fields['hook_watch'] = !empty($_POST['hook_watch']) ? 1 : 0;
$fields['hook_critical'] = !empty($_POST['hook_critical']) ? 1 : 0;
$fields['hook_kill_game_if_app_exits'] = !empty($_POST['hook_kill_game_if_app_exits']) ? 1 : 0;
$fields['hook_restart_app_if_exits'] = !empty($_POST['hook_restart_app_if_exits']) ? 1 : 0;
$fields['hook_pid_name'] = isset($_POST['hook_pid_name']) ? trim((string)$_POST['hook_pid_name']) : '';
$fields['hook_app_name'] = isset($_POST['hook_app_name']) ? trim((string)$_POST['hook_app_name']) : '';
$fields['hook_description'] = isset($_POST['hook_description']) ? trim((string)$_POST['hook_description']) : '';
$fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']);
if ($fields['install_method'] === 'steam_workshop') {
$fields['url'] = '';
@ -102,6 +117,21 @@ function exec_ogp_module() {
'target_path_template' => $fields['target_path_template'],
'post_script' => $fields['post_script'],
'config_edit_rule' => $fields['config_edit_rule'],
'hook_name' => $fields['hook_name'],
'hook_enabled' => $fields['hook_enabled'],
'hook_platform' => $fields['hook_platform'],
'hook_working_dir' => $fields['hook_working_dir'],
'hook_start_command' => $fields['hook_start_command'],
'hook_stop_command' => $fields['hook_stop_command'],
'hook_start_timing' => $fields['hook_start_timing'],
'hook_stop_with_server' => $fields['hook_stop_with_server'],
'hook_watch' => $fields['hook_watch'],
'hook_critical' => $fields['hook_critical'],
'hook_kill_game_if_app_exits' => $fields['hook_kill_game_if_app_exits'],
'hook_restart_app_if_exits' => $fields['hook_restart_app_if_exits'],
'hook_pid_name' => $fields['hook_pid_name'],
'hook_app_name' => $fields['hook_app_name'],
'hook_description' => $fields['hook_description'],
);
$validation_message = '';
if (!scm_validate_install_method_payload($fields['install_method'], $validation_payload, $validation_message))
@ -133,6 +163,21 @@ function exec_ogp_module() {
$is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0;
$description = isset($_POST['description']) ? $_POST['description'] : "";
$config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : "";
$hook_name = isset($_POST['hook_name']) ? $_POST['hook_name'] : "";
$hook_enabled = isset($_POST['hook_enabled']) ? (int)$_POST['hook_enabled'] : 1;
$hook_platform = isset($_POST['hook_platform']) ? $_POST['hook_platform'] : "both";
$hook_working_dir = isset($_POST['hook_working_dir']) ? $_POST['hook_working_dir'] : "";
$hook_start_command = isset($_POST['hook_start_command']) ? $_POST['hook_start_command'] : "";
$hook_stop_command = isset($_POST['hook_stop_command']) ? $_POST['hook_stop_command'] : "";
$hook_start_timing = isset($_POST['hook_start_timing']) ? $_POST['hook_start_timing'] : "before_server";
$hook_stop_with_server = isset($_POST['hook_stop_with_server']) ? (int)$_POST['hook_stop_with_server'] : 1;
$hook_watch = isset($_POST['hook_watch']) ? (int)$_POST['hook_watch'] : 1;
$hook_critical = isset($_POST['hook_critical']) ? (int)$_POST['hook_critical'] : 0;
$hook_kill_game_if_app_exits = isset($_POST['hook_kill_game_if_app_exits']) ? (int)$_POST['hook_kill_game_if_app_exits'] : 0;
$hook_restart_app_if_exits = isset($_POST['hook_restart_app_if_exits']) ? (int)$_POST['hook_restart_app_if_exits'] : 1;
$hook_pid_name = isset($_POST['hook_pid_name']) ? $_POST['hook_pid_name'] : "";
$hook_app_name = isset($_POST['hook_app_name']) ? $_POST['hook_app_name'] : "";
$hook_description = isset($_POST['hook_description']) ? $_POST['hook_description'] : "";
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
{
@ -156,6 +201,21 @@ function exec_ogp_module() {
$is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0;
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
$config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : "";
$hook_name = isset($addon_info['hook_name']) ? $addon_info['hook_name'] : "";
$hook_enabled = isset($addon_info['hook_enabled']) ? (int)$addon_info['hook_enabled'] : 1;
$hook_platform = isset($addon_info['hook_platform']) ? $addon_info['hook_platform'] : "both";
$hook_working_dir = isset($addon_info['hook_working_dir']) ? $addon_info['hook_working_dir'] : "";
$hook_start_command = isset($addon_info['hook_start_command']) ? $addon_info['hook_start_command'] : "";
$hook_stop_command = isset($addon_info['hook_stop_command']) ? $addon_info['hook_stop_command'] : "";
$hook_start_timing = isset($addon_info['hook_start_timing']) ? $addon_info['hook_start_timing'] : "before_server";
$hook_stop_with_server = isset($addon_info['hook_stop_with_server']) ? (int)$addon_info['hook_stop_with_server'] : 1;
$hook_watch = isset($addon_info['hook_watch']) ? (int)$addon_info['hook_watch'] : 1;
$hook_critical = isset($addon_info['hook_critical']) ? (int)$addon_info['hook_critical'] : 0;
$hook_kill_game_if_app_exits = isset($addon_info['hook_kill_game_if_app_exits']) ? (int)$addon_info['hook_kill_game_if_app_exits'] : 0;
$hook_restart_app_if_exits = isset($addon_info['hook_restart_app_if_exits']) ? (int)$addon_info['hook_restart_app_if_exits'] : 1;
$hook_pid_name = isset($addon_info['hook_pid_name']) ? $addon_info['hook_pid_name'] : "";
$hook_app_name = isset($addon_info['hook_app_name']) ? $addon_info['hook_app_name'] : "";
$hook_description = isset($addon_info['hook_description']) ? $addon_info['hook_description'] : "";
}
?>
<form action="" method="post">
@ -237,6 +297,78 @@ function exec_ogp_module() {
<textarea name="config_edit_rule" style="width:99%;height:90px;" placeholder="Text/rules to append or apply to the target config."><?php echo htmlspecialchars($config_edit_rule, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Application name</b></td>
<td align="left">
<input type="text" name="hook_name" value="<?php echo htmlspecialchars($hook_name, ENT_QUOTES, 'UTF-8'); ?>" size="60" placeholder="BEC" />
<label style="margin-left:10px;">
<input type="checkbox" name="hook_enabled" value="1" <?php echo $hook_enabled ? 'checked' : ''; ?> />
Enabled
</label>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Platform</b></td>
<td align="left">
<select name="hook_platform">
<option value="both" <?php echo $hook_platform === 'both' ? 'selected' : ''; ?>>Both</option>
<option value="windows" <?php echo $hook_platform === 'windows' ? 'selected' : ''; ?>>Windows</option>
<option value="linux" <?php echo $hook_platform === 'linux' ? 'selected' : ''; ?>>Linux</option>
</select>
&nbsp;
<select name="hook_start_timing">
<option value="before_server" <?php echo $hook_start_timing === 'before_server' ? 'selected' : ''; ?>>Start before server</option>
<option value="after_server" <?php echo $hook_start_timing === 'after_server' ? 'selected' : ''; ?>>Start after server</option>
</select>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Working directory</b></td>
<td align="left">
<input type="text" name="hook_working_dir" value="<?php echo htmlspecialchars($hook_working_dir, ENT_QUOTES, 'UTF-8'); ?>" size="85" placeholder="bec" />
<small style="color:#666;">Relative to the server home unless an absolute path is required.</small>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Start command</b></td>
<td align="left">
<input type="text" name="hook_start_command" value="<?php echo htmlspecialchars($hook_start_command, ENT_QUOTES, 'UTF-8'); ?>" size="85" placeholder="BEC.exe -f Config.cfg" />
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Stop command</b></td>
<td align="left">
<input type="text" name="hook_stop_command" value="<?php echo htmlspecialchars($hook_stop_command, ENT_QUOTES, 'UTF-8'); ?>" size="85" placeholder="Optional graceful stop command" />
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>PID / app names</b></td>
<td align="left">
<input type="text" name="hook_pid_name" value="<?php echo htmlspecialchars($hook_pid_name, ENT_QUOTES, 'UTF-8'); ?>" size="35" placeholder="bec" />
<input type="text" name="hook_app_name" value="<?php echo htmlspecialchars($hook_app_name, ENT_QUOTES, 'UTF-8'); ?>" size="35" placeholder="BEC.exe" />
<small style="color:#666;">Optional names used in runtime PID records and fallback cleanup.</small>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Lifecycle behavior</b></td>
<td align="left">
<label><input type="checkbox" name="hook_stop_with_server" value="1" <?php echo $hook_stop_with_server ? 'checked' : ''; ?> /> Stop with server</label>
&nbsp;&nbsp;
<label><input type="checkbox" name="hook_watch" value="1" <?php echo $hook_watch ? 'checked' : ''; ?> /> Watch app</label>
&nbsp;&nbsp;
<label><input type="checkbox" name="hook_restart_app_if_exits" value="1" <?php echo $hook_restart_app_if_exits ? 'checked' : ''; ?> /> Restart app if it exits</label>
<br>
<label><input type="checkbox" name="hook_critical" value="1" <?php echo $hook_critical ? 'checked' : ''; ?> /> Critical</label>
&nbsp;&nbsp;
<label><input type="checkbox" name="hook_kill_game_if_app_exits" value="1" <?php echo $hook_kill_game_if_app_exits ? 'checked' : ''; ?> /> Kill game if app exits</label>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Hook description</b></td>
<td align="left">
<textarea name="hook_description" style="width:99%;height:55px;" placeholder="Battleye Extended Controls watchdog for DayZ/Arma servers"><?php echo htmlspecialchars($hook_description, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('select_game_type'); ?></b>

View file

@ -42,6 +42,7 @@ function get_server_content_categories()
'file_download' => 'Downloadable Mod',
'config_edit' => 'Configuration Package',
'scripted_installer' => 'Scripted Installer',
'server_app' => 'Server-side Application',
);
}
@ -79,6 +80,7 @@ function scm_get_addon_type_from_install_method($install_method)
'download_zip' => 'file_download',
'config_edit' => 'config_edit',
'post_script' => 'scripted_installer',
'server_app' => 'server_app',
);
return isset($map[$install_method]) ? $map[$install_method] : 'file_download';
}

View file

@ -696,6 +696,7 @@ function scm_get_install_methods()
'download_zip' => 'Downloadable Mod',
'config_edit' => 'Configuration Package',
'post_script' => 'Scripted Installer',
'server_app' => 'Server-side Application',
);
}
@ -705,6 +706,7 @@ function scm_get_install_method_help_text()
'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
'config_edit' => 'Install configuration files, profiles, or templates.',
'post_script' => 'Run a custom scripted installation process.',
'server_app' => 'Install a server-side application hook managed by the agent lifecycle.',
);
}
@ -715,6 +717,7 @@ function scm_get_install_method_required_fields()
'steam_workshop' => array(), // No required fields; users provide Workshop IDs on their server page
'post_script' => array('post_script'),
'config_edit' => array('path', 'config_edit_rule'),
'server_app' => array('hook_name', 'hook_platform', 'hook_working_dir', 'hook_start_command'),
);
}
@ -724,6 +727,7 @@ function scm_get_install_method_validation_errors()
'download_zip' => 'Please enter a download URL.',
'config_edit' => 'Please enter the config target and edit action.',
'post_script' => 'Please enter the installer script/action.',
'server_app' => 'Please enter the application name, platform, working directory, and start command.',
);
}
@ -752,6 +756,21 @@ function scm_get_install_payload_keys()
'post_script',
'config_edit_rule',
'launch_param_additions',
'hook_name',
'hook_enabled',
'hook_platform',
'hook_working_dir',
'hook_start_command',
'hook_stop_command',
'hook_start_timing',
'hook_stop_with_server',
'hook_watch',
'hook_critical',
'hook_kill_game_if_app_exits',
'hook_restart_app_if_exits',
'hook_pid_name',
'hook_app_name',
'hook_description',
);
}
@ -844,6 +863,83 @@ function scm_validate_configuration_package(array $payload, &$message = '')
return true;
}
function scm_normalize_hook_platform($platform)
{
$platform = strtolower(trim((string)$platform));
return in_array($platform, array('windows', 'linux', 'both'), true) ? $platform : 'both';
}
function scm_normalize_hook_start_timing($start_timing)
{
$start_timing = strtolower(trim((string)$start_timing));
return in_array($start_timing, array('before_server', 'after_server'), true) ? $start_timing : 'before_server';
}
function scm_bool_to_int($value)
{
return !empty($value) ? 1 : 0;
}
function scm_hook_safe_filename($name)
{
$name = strtolower(trim((string)$name));
$name = preg_replace('/[^a-z0-9._-]+/', '_', $name);
$name = trim($name, '._-');
return $name !== '' ? $name : 'server_app';
}
function scm_validate_server_app_content(array $payload, &$message = '')
{
$name = isset($payload['hook_name']) ? trim((string)$payload['hook_name']) : '';
$working_dir = isset($payload['hook_working_dir']) ? trim((string)$payload['hook_working_dir']) : '';
$start_command = isset($payload['hook_start_command']) ? trim((string)$payload['hook_start_command']) : '';
if ($name === '' || $working_dir === '' || $start_command === '') {
$message = 'Please enter the application name, working directory, and start command.';
return false;
}
if (strpos($working_dir, '..') !== false) {
$message = 'Hook working directory must not contain parent-directory traversal.';
return false;
}
$platform = scm_normalize_hook_platform(isset($payload['hook_platform']) ? $payload['hook_platform'] : '');
if (!in_array($platform, array('windows', 'linux', 'both'), true)) {
$message = 'Hook platform must be Windows, Linux, or Both.';
return false;
}
$message = '';
return true;
}
function scm_build_server_app_hook_manifest(array $addon_info)
{
$name = trim((string)(isset($addon_info['hook_name']) && $addon_info['hook_name'] !== '' ? $addon_info['hook_name'] : (isset($addon_info['name']) ? $addon_info['name'] : 'Server App')));
$pid_name = trim((string)(isset($addon_info['hook_pid_name']) ? $addon_info['hook_pid_name'] : ''));
$app_name = trim((string)(isset($addon_info['hook_app_name']) ? $addon_info['hook_app_name'] : ''));
if ($pid_name === '') {
$pid_name = scm_hook_safe_filename($name);
}
if ($app_name === '') {
$app_name = $pid_name;
}
return array(
'name' => $name,
'enabled' => !empty($addon_info['hook_enabled']),
'platform' => scm_normalize_hook_platform(isset($addon_info['hook_platform']) ? $addon_info['hook_platform'] : 'both'),
'working_dir' => trim((string)(isset($addon_info['hook_working_dir']) ? $addon_info['hook_working_dir'] : '')),
'start_command' => trim((string)(isset($addon_info['hook_start_command']) ? $addon_info['hook_start_command'] : '')),
'stop_command' => trim((string)(isset($addon_info['hook_stop_command']) ? $addon_info['hook_stop_command'] : '')),
'start_timing' => scm_normalize_hook_start_timing(isset($addon_info['hook_start_timing']) ? $addon_info['hook_start_timing'] : 'before_server'),
'stop_with_server' => !empty($addon_info['hook_stop_with_server']),
'watch' => !empty($addon_info['hook_watch']),
'critical' => !empty($addon_info['hook_critical']),
'kill_game_if_app_exits' => !empty($addon_info['hook_kill_game_if_app_exits']),
'restart_app_if_exits' => !empty($addon_info['hook_restart_app_if_exits']),
'pid_name' => $pid_name,
'app_name' => $app_name,
'description' => trim((string)(isset($addon_info['hook_description']) ? $addon_info['hook_description'] : '')),
);
}
function scm_validate_install_method_payload($install_method, array $payload, &$message = '')
{
$install_method = scm_get_install_method_default($install_method);
@ -864,6 +960,9 @@ function scm_validate_install_method_payload($install_method, array $payload, &$
if ($install_method === 'config_edit') {
return scm_validate_configuration_package($payload, $message);
}
if ($install_method === 'server_app') {
return scm_validate_server_app_content($payload, $message);
}
$message = '';
return true;
}
@ -1097,6 +1196,21 @@ function scm_ensure_phase2_schema($db)
'max_workshop_ids' => "INT NULL",
'required_workshop_ids' => "TEXT NULL",
'blocked_workshop_ids' => "TEXT NULL",
'hook_name' => "VARCHAR(128) NULL",
'hook_enabled' => "TINYINT(1) NOT NULL DEFAULT 1",
'hook_platform' => "VARCHAR(16) NOT NULL DEFAULT 'both'",
'hook_working_dir' => "VARCHAR(255) NULL",
'hook_start_command' => "TEXT NULL",
'hook_stop_command' => "TEXT NULL",
'hook_start_timing' => "VARCHAR(32) NOT NULL DEFAULT 'before_server'",
'hook_stop_with_server' => "TINYINT(1) NOT NULL DEFAULT 1",
'hook_watch' => "TINYINT(1) NOT NULL DEFAULT 1",
'hook_critical' => "TINYINT(1) NOT NULL DEFAULT 0",
'hook_kill_game_if_app_exits' => "TINYINT(1) NOT NULL DEFAULT 0",
'hook_restart_app_if_exits' => "TINYINT(1) NOT NULL DEFAULT 1",
'hook_pid_name' => "VARCHAR(128) NULL",
'hook_app_name' => "VARCHAR(128) NULL",
'hook_description' => "TEXT NULL",
);
foreach ($new_columns as $col => $definition) {
$escaped_col = $db->realEscapeSingle($col);
@ -1296,3 +1410,55 @@ function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array())
updated_at = NOW()"
);
}
function scm_remote_shell_quote($value)
{
return "'" . str_replace("'", "'\"'\"'", (string)$value) . "'";
}
function scm_install_server_app_hook($remote, array $home_info, array $addon_info, &$message = '')
{
$home_path = rtrim((string)(isset($home_info['home_path']) ? $home_info['home_path'] : ''), '/\\');
if ($home_path === '') {
$message = 'Server home path is missing.';
return false;
}
$manifest = scm_build_server_app_hook_manifest($addon_info);
$validation_message = '';
if (!scm_validate_server_app_content(array(
'hook_name' => $manifest['name'],
'hook_platform' => $manifest['platform'],
'hook_working_dir' => $manifest['working_dir'],
'hook_start_command' => $manifest['start_command'],
), $validation_message)) {
$message = $validation_message;
return false;
}
$json = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($json === false) {
$message = 'Failed to encode server application hook manifest.';
return false;
}
$base = $home_path . '/_gsp_content';
$mkdir = 'mkdir -p ' .
scm_remote_shell_quote($base . '/hooks') . ' ' .
scm_remote_shell_quote($base . '/generated') . ' ' .
scm_remote_shell_quote($base . '/runtime');
$mkdir_result = $remote->exec($mkdir . ' && echo __GSP_HOOK_DIRS_OK');
if (!is_string($mkdir_result) || strpos($mkdir_result, '__GSP_HOOK_DIRS_OK') === false) {
$message = 'Failed to create server content hook directories.';
return false;
}
$filename = scm_hook_safe_filename($manifest['pid_name'] !== '' ? $manifest['pid_name'] : $manifest['name']) . '.json';
$write_path = $base . '/hooks/' . $filename;
$write_result = $remote->remote_writefile($write_path, $json . PHP_EOL);
if ((int)$write_result !== 1) {
$message = 'Failed to write hook manifest to ' . $write_path . '.';
return false;
}
$message = $write_path;
return true;
}

View file

@ -0,0 +1,90 @@
# Server Content Application Hooks
Server Content Application Hooks let installed server content manage companion
applications such as BEC, Big Brother Bot, Discord bridges, RCON tools, and log
watchers as part of the game server lifecycle.
The application files may live wherever the game requires them. GSP lifecycle
metadata lives under each server home:
```text
_gsp_content/
hooks/
generated/
runtime/
```
## Content Type
The Server Content Manager includes the `Server-side Application` content type.
When this type is installed, the Panel writes a JSON hook manifest to:
```text
_gsp_content/hooks/<app>.json
```
The agents read these manifests during server startup.
## Hook Manifest
```json
{
"name": "BEC",
"enabled": true,
"platform": "windows",
"working_dir": "bec",
"start_command": "BEC.exe -f Config.cfg",
"stop_command": "",
"start_timing": "before_server",
"stop_with_server": true,
"watch": true,
"critical": true,
"kill_game_if_app_exits": false,
"restart_app_if_exits": true,
"pid_name": "bec",
"app_name": "BEC.exe",
"description": "Battleye Extended Controls watchdog for DayZ/Arma servers"
}
```
Supported `platform` values are `windows`, `linux`, and `both`.
Supported `start_timing` values are `before_server` and `after_server`.
## Runtime PID File
Agents write hook runtime PIDs to:
```text
_gsp_content/runtime/server_content.pids
```
Format:
```text
watchdog|BEC|12345
app|BEC|12346
```
The main game server watchdog PID is not stored in this file.
## Lifecycle
On server start, the agent:
1. Creates `_gsp_content/hooks`, `_gsp_content/generated`, and `_gsp_content/runtime`.
2. Reads enabled hook manifests matching the agent platform.
3. Generates platform-specific hook watchdog scripts.
4. Starts `before_server` hooks.
5. Starts the game server.
6. Starts `after_server` hooks.
7. Cleans up hook watchdogs and apps when the game server exits.
On Panel Stop or Restart, agents kill hook watchdog PIDs first and hook app PIDs
second, then continue normal game process and screen/session cleanup.
## Legacy `_alsoRun.bat`
Windows `_alsoRun.bat` support remains for compatibility, but it is deprecated.
New companion applications should be installed as `Server-side Application`
content so both Linux and Windows agents can manage them through the same hook
contract.

View file

@ -33,10 +33,31 @@ The module can already represent several content types, including:
- downloads/extracted packages
- post-script driven installs
- config packs
- server-side applications with lifecycle hooks
- future profile-type content
Steam Workshop is no longer a user-facing Server Content category. Workshop access belongs to the dedicated `steam_workshop` module.
## Server-Side Applications
`Server-side Application` content writes an agent-readable hook manifest under
the target game home:
```text
_gsp_content/hooks/<app>.json
```
The agents generate runtime watchdog scripts in `_gsp_content/generated/` and
track side-application PIDs in `_gsp_content/runtime/server_content.pids`.
Use this type for companion applications such as BEC, Big Brother Bot, Discord
bridges, RCON tools, and log watchers. The application files themselves may
still be installed wherever the game requires them.
Detailed lifecycle documentation:
- `docs/features/SERVER_CONTENT_APPLICATION_HOOKS.md`
## Current Limitations
- Cache and cleanup policy need a clearer product design.
@ -56,6 +77,7 @@ This module is the right place for:
- add-ons
- config packs
- script-driven installs
- server-side companion application hooks
- server content manifests
- install history