worshop work

This commit is contained in:
Frank Harris 2026-06-08 16:09:54 -05:00
parent 0d44c65ea5
commit 3829a4a83d
92 changed files with 487 additions and 110 deletions

View file

@ -16,14 +16,12 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
- Update All
- Panel generates a per-server manifest at:
- `%home_path%/gsp_server_content/workshop_manifest.json`
- Panel runs an approved handler only, never a user-supplied command/path.
- If a game does not define a custom Workshop script, the panel stages the
bundled generic handler for the agent OS:
- Linux: `generic_steam_workshop_linux.sh`
- Windows/Cygwin: `generic_steam_workshop_windows_cygwin.sh`
- If a custom configured script is missing on the agent, the panel falls back
to the bundled generic handler and logs a warning instead of failing with
"script not found."
- Panel generates an approved per-job handler only, never a user-supplied command/path.
- The generated job is written under:
- `%home_path%/gsp_server_content/jobs/workshop/workshop_job_<timestamp>_<random>.sh`
- The generated job writes a temporary SteamCMD runscript and invokes SteamCMD with `+runscript`.
- Static Workshop script paths in XML are deprecated for the primary workflow.
- The agent does not need `generic_steam_workshop_linux.sh` or `generic_steam_workshop_windows_cygwin.sh` to exist on disk.
## Security model
@ -51,10 +49,7 @@ and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is val
Each game should define and document:
- `workshop_app_id`
- optional Linux workshop script path only when the bundled generic handler is
not sufficient
- optional Windows/Cygwin workshop script path only when the bundled generic
handler is not sufficient
- install strategy and target path
- target install location
- restart/update behavior

View file

@ -13,6 +13,7 @@ import os
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
manifest_path = os.path.abspath(sys.argv[1])
@ -192,14 +193,16 @@ try:
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
command = [
steamcmd_path,
'+force_install_dir', server_root,
'+login', 'anonymous',
'+workshop_download_item', workshop_app_id, workshop_id, 'validate',
'+quit',
]
log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", 'Downloading Workshop Item')
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
script_handle.write("@ShutdownOnFailedCommand 0\n")
script_handle.write("@NoPromptForPassword 1\n")
script_handle.write(f"force_install_dir {server_root}\n")
script_handle.write("login anonymous\n")
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
script_handle.write("quit\n")
command = [steamcmd_path, '+runscript', runscript_path]
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if result.stdout:
for line in result.stdout.splitlines():

View file

@ -13,6 +13,7 @@ import os
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
manifest_path = os.path.abspath(sys.argv[1])
@ -193,14 +194,16 @@ try:
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
command = [
steamcmd_path,
'+force_install_dir', server_root,
'+login', 'anonymous',
'+workshop_download_item', workshop_app_id, workshop_id, 'validate',
'+quit',
]
log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", 'Downloading Workshop Item')
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
script_handle.write("@ShutdownOnFailedCommand 0\n")
script_handle.write("@NoPromptForPassword 1\n")
script_handle.write(f"force_install_dir {server_root}\n")
script_handle.write("login anonymous\n")
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
script_handle.write("quit\n")
command = [steamcmd_path, '+runscript', runscript_path]
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if result.stdout:
for line in result.stdout.splitlines():

View file

@ -507,8 +507,8 @@ function scm_is_legacy_panel_workshop_script_path($script_path)
function scm_get_agent_managed_workshop_script_path(array $home_info)
{
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
$remote_path = clean_path($home_path . '/gsp_server_content/scripts/workshop/' . $filename);
$filename = 'workshop_job_' . date('Ymd_His') . '_' . mt_rand(1000, 9999) . '.sh';
$remote_path = clean_path($home_path . '/gsp_server_content/jobs/workshop/' . $filename);
if (!scm_path_is_under_home($home_path, $remote_path)) {
return false;
}
@ -520,20 +520,17 @@ function scm_prepare_workshop_script_for_agent($remote, array $home_info, $serve
$error = '';
$configured_path = scm_get_configured_workshop_script_path($home_info, $server_xml);
if ($configured_path !== '' && !scm_is_default_workshop_script_name($configured_path) && !scm_is_legacy_panel_workshop_script_path($configured_path)) {
if ((int)$remote->rfile_exists($configured_path) === 1) {
return $configured_path;
}
scm_log_content_install_action(array(
'type' => 'workshop_script_fallback',
'type' => 'workshop_script_deprecated',
'home_id' => isset($home_info['home_id']) ? (int)$home_info['home_id'] : 0,
'configured_path' => $configured_path,
'message' => 'Configured workshop script was not found on the agent; falling back to bundled generic handler.',
'message' => 'Configured static Workshop script ignored; Server Content generates a per-job script and runs it through the generic agent exec path.',
));
}
$source_path = scm_get_bundled_workshop_script_source($home_info);
if (!is_file($source_path)) {
$error = 'Bundled workshop script is missing from the panel: ' . $source_path;
$error = 'Panel Workshop job template is missing: ' . $source_path;
return false;
}
@ -545,7 +542,7 @@ function scm_prepare_workshop_script_for_agent($remote, array $home_info, $serve
$script_body = @file_get_contents($source_path);
if ($script_body === false || $script_body === '') {
$error = 'Failed to read bundled workshop script: ' . $source_path;
$error = 'Failed to read Panel Workshop job template: ' . $source_path;
return false;
}

View file

@ -75,26 +75,29 @@ $emptyXml = simplexml_load_string('<game_config></game_config>');
$linuxRemote = new ScmWorkshopFakeRemote();
$error = '';
$script = scm_prepare_workshop_script_for_agent($linuxRemote, $linuxHome, $emptyXml, $error);
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'stages default Linux script under server home');
scm_workshop_test_assert(isset($linuxRemote->files[$script]), 'writes Linux bundled script to fake agent');
scm_workshop_test_assert($error === '', 'default Linux script staging does not report missing script');
scm_workshop_test_assert(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'stages per-job Linux script under server home');
scm_workshop_test_assert(isset($linuxRemote->files[$script]), 'writes Linux per-job script to fake agent');
scm_workshop_test_assert(strpos($linuxRemote->files[$script], '+runscript') !== false, 'Linux per-job script runs SteamCMD through runscript');
scm_workshop_test_assert(strpos($linuxRemote->files[$script], 'workshop_download_item {workshop_app_id} {workshop_id} validate') !== false, 'Linux per-job script generates workshop_download_item runscript command');
scm_workshop_test_assert($error === '', 'default Linux job staging does not report missing agent script');
$windowsRemote = new ScmWorkshopFakeRemote();
$script = scm_prepare_workshop_script_for_agent($windowsRemote, $windowsHome, $emptyXml, $error);
scm_workshop_test_assert($script === '/cygdrive/c/OGP_User_Files/11/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh', 'stages default Windows/Cygwin script under server home');
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin bundled script to fake agent');
scm_workshop_test_assert($error === '', 'default Windows/Cygwin script staging does not report missing script');
scm_workshop_test_assert(strpos($script, '/cygdrive/c/OGP_User_Files/11/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'stages per-job Windows/Cygwin script under server home');
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin per-job script to fake agent');
scm_workshop_test_assert(strpos($windowsRemote->files[$script], '+runscript') !== false, 'Windows/Cygwin per-job script runs SteamCMD through runscript');
scm_workshop_test_assert($error === '', 'default Windows/Cygwin job staging does not report missing agent script');
$configuredXml = simplexml_load_string('<game_config><workshop_support><script_linux>/agent/custom/workshop.sh</script_linux></workshop_support></game_config>');
$customRemote = new ScmWorkshopFakeRemote();
$customRemote->existing[] = '/agent/custom/workshop.sh';
$script = scm_prepare_workshop_script_for_agent($customRemote, $linuxHome, $configuredXml, $error);
scm_workshop_test_assert($script === '/agent/custom/workshop.sh', 'uses existing configured custom script');
scm_workshop_test_assert(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'ignores configured static script and uses generated per-job script');
$missingCustomRemote = new ScmWorkshopFakeRemote();
$script = scm_prepare_workshop_script_for_agent($missingCustomRemote, $linuxHome, $configuredXml, $error);
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'falls back to bundled script when configured custom script is missing');
scm_workshop_test_assert($error === '', 'missing custom script fallback does not expose script-not-found error');
scm_workshop_test_assert(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'missing custom static script still uses generated per-job script');
scm_workshop_test_assert($error === '', 'missing custom script does not expose script-not-found error');
@unlink(dirname(__DIR__) . '/logs/content_install.log');
@rmdir(dirname(__DIR__) . '/logs');