Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions server/src/utils/ptyControlChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
unlink
} from 'node:fs/promises'
import net from 'node:net'
import os from 'node:os'
import path from 'node:path'

export interface CreatePtyControlChannelOptions {
Expand Down Expand Up @@ -1229,16 +1230,28 @@ class WindowsPtyControlTransport implements PtyControlTransport {
}
}

function getDefaultControlDirectoryCandidates(): string[] {
const baseDir = process.cwd()
const effectiveUserId = typeof process.geteuid === 'function'
? String(process.geteuid())
: 'unknown'
const candidates = [
path.join(baseDir, 'data', 'terminal-control'),
path.join(baseDir, 'server', 'data', 'terminal-control'),
path.join(baseDir, '..', 'server', 'data', 'terminal-control'),
path.join(os.homedir(), '.gsm3', 'terminal-control'),
path.join(os.tmpdir(), `gsm3-terminal-control-${effectiveUserId}`)
]

return [...new Set(candidates.map((candidate) => path.resolve(candidate)))]
}

async function selectPosixControlDirectory(
options: CreatePtyControlChannelOptions,
platform: NodeJS.Platform,
securityFlags: PosixSecurityFlags
): Promise<PosixControlDirectory> {
const candidates = [
path.join(process.cwd(), 'data', 'terminal-control'),
path.join(process.cwd(), 'server', 'data', 'terminal-control')
]
const directoryCandidates = options.directoryCandidates ?? candidates
const directoryCandidates = options.directoryCandidates ?? getDefaultControlDirectoryCandidates()

for (const rawCandidate of directoryCandidates) {
let descriptor: number | null = null
Expand Down Expand Up @@ -1304,7 +1317,9 @@ async function selectPosixControlDirectory(

throw new PtyControlStageError(
'directory',
`PTY control directory unavailable platform=${platform} sessionId=${options.sessionId} stage=directory`
`PTY control directory unavailable platform=${platform} sessionId=${options.sessionId} stage=directory; ` +
'checked paths under data/, ~/.gsm3/, and $TMPDIR. ' +
'If data/terminal-control was created by another user (e.g. root), remove it or chown it to the panel runtime user.'
)
}

Expand Down
41 changes: 33 additions & 8 deletions server/src/utils/ptyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,29 @@ class PtyManager {

/**
* 获取 lib 目录的候选路径列表。
* 顺序兼容打包后环境和开发环境,不得改变
* 顺序兼容打包、开发、Docker 内置资产和旧版 Docker 布局
*/
private getLibDirCandidates(): string[] {
const candidates = [
path.join(process.cwd(), 'data', 'lib'),
path.join(process.cwd(), 'server', 'data', 'lib')
const baseDir = process.cwd()
return Array.from(new Set([
...this.getWritableLibDirCandidates(),
path.join(baseDir, 'builtin', 'data', 'lib'), // 当前 Docker 镜像内置资产目录
path.join(baseDir, '..', 'data', 'lib') // 兼容旧版 Docker/启动脚本布局
]))
}

/** 获取允许服务端下载或替换资产的运行时目录。 */
private getWritableLibDirCandidates(): string[] {
const baseDir = process.cwd()
return [
path.join(baseDir, 'data', 'lib'),
path.join(baseDir, 'server', 'data', 'lib')
]
return candidates
}

/** 优先使用第一个已存在目录;均不存在时创建第一个可写目录。 */
private async getTargetDir(): Promise<string> {
const candidates = this.getLibDirCandidates()
const candidates = this.getWritableLibDirCandidates()

for (const candidate of candidates) {
try {
Expand Down Expand Up @@ -69,6 +79,21 @@ class PtyManager {
*/
async getPtyPath(): Promise<string> {
const asset = getPtyAsset()

for (const candidate of this.getLibDirCandidates()) {
const targetPath = path.join(candidate, asset.name)
if (!await verifyPtyAsset(targetPath, asset)) {
continue
}

try {
await probePtyAsset(targetPath, asset)
return targetPath
} catch {
logger.warn(`PTY 资产能力探测失败,将尝试其他路径: ${targetPath}`)
}
}

const targetDir = await this.getTargetDir()
return ensurePtyAsset({ asset, targetDir, logger })
}
Expand All @@ -92,13 +117,13 @@ class PtyManager {

const targetPath = path.join(candidate, asset.name)
if (!await verifyPtyAsset(targetPath, asset)) {
return false
continue
}
try {
await probePtyAsset(targetPath, asset)
return true
} catch {
return false
continue
}
}

Expand Down
22 changes: 16 additions & 6 deletions server/src/utils/zipToolsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,12 @@ class ZipToolsManager {
*/
private getLibDirCandidates(): string[] {
const baseDir = process.cwd()
return [
path.join(baseDir, 'data', 'lib'), // 打包后环境
path.join(baseDir, 'server', 'data', 'lib'), // 开发环境
]
return Array.from(new Set([
path.join(baseDir, 'data', 'lib'), // 打包后或 Docker 运行时目录
path.join(baseDir, 'server', 'data', 'lib'), // 开发环境目录
path.join(baseDir, 'builtin', 'data', 'lib'), // 当前 Docker 镜像内置资产目录
path.join(baseDir, '..', 'data', 'lib'), // 兼容旧版 Docker/启动脚本布局
]))
}

/**
Expand Down Expand Up @@ -230,7 +232,11 @@ class ZipToolsManager {
logger.info(`Zip-Tools 下载完成: ${targetPath}`)
} catch (error: any) {
// 清理可能的残留文件
try { await fs.unlink(targetPath) } catch { /* 忽略 */ }
try {
await fs.unlink(targetPath)
} catch (cleanupError) {
logger.debug(`清理 Zip-Tools 残留文件失败: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`)
}
const message = `Zip-Tools 下载失败(GitHub): ${error.message || error}`
logger.error(message)
throw new Error(message)
Expand Down Expand Up @@ -342,7 +348,11 @@ class ZipToolsManager {
logger.info(`7z 下载完成: ${targetPath}`)
} catch (error: any) {
// 清理可能的残留文件
try { await fs.unlink(targetPath) } catch { /* 忽略 */ }
try {
await fs.unlink(targetPath)
} catch (cleanupError) {
logger.debug(`清理 7z 残留文件失败: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`)
}
const message = `7z 下载失败(GitHub): ${error.message || error}`
logger.error(message)
throw new Error(message)
Expand Down
51 changes: 23 additions & 28 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ echo " GSM3 游戏服务端管理面板"
echo "======================================"
echo

# 将打包内置的 lib 资产同步到运行时目录(Docker 数据卷会覆盖 server/data/lib)
seed_runtime_lib_assets() {
local runtime_lib="$1"
local builtin_lib="$2"

mkdir -p "$runtime_lib"
if [ ! -d "$builtin_lib" ]; then
return
fi

for asset in "$builtin_lib"/*; do
[ -f "$asset" ] || continue
local dest="$runtime_lib/$(basename "$asset")"
if [ ! -e "$dest" ]; then
cp -a "$asset" "$dest"
fi
done
}

# 检查是否存在GSM3应用文件
if [ -f "server/index.js" ]; then
echo "🚀 启动GSM3管理面板..."
Expand Down Expand Up @@ -37,42 +56,18 @@ if [ -f "server/index.js" ]; then
# Docker 的持久卷会遮蔽镜像内的 server/data,补充卷中缺失的内置运行时资产。
BUILTIN_LIB_DIR="server/builtin/data/lib"
RUNTIME_LIB_DIR="server/data/lib"
if [ -d "$BUILTIN_LIB_DIR" ]; then
mkdir -p "$RUNTIME_LIB_DIR"
cp -an "$BUILTIN_LIB_DIR"/. "$RUNTIME_LIB_DIR"/ 2>/dev/null || true
fi
seed_runtime_lib_assets "$RUNTIME_LIB_DIR" "$BUILTIN_LIB_DIR"

# PTY 文件已迁移到 data/lib/ 目录,启动时由服务端自动检测和下载
# 如果 data/lib/ 中存在 PTY 文件,验证并设置可执行权限
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
PTY_FILE="$RUNTIME_LIB_DIR/pty_linux_x64"
elif [ "$ARCH" = "aarch64" ]; then
PTY_FILE="$RUNTIME_LIB_DIR/pty_linux_arm64"
else
PTY_FILE=""
fi
# PTY 由服务端固定资产管理器校验和探测,避免依赖镜像中未安装的 file 命令。
# 内置资产缺失或损坏时,服务端会自动选择可写目录并恢复固定版本。

if [ -n "$PTY_FILE" ] && [ -f "$PTY_FILE" ]; then
# 验证是否为有效的ELF二进制文件
if file "$PTY_FILE" 2>/dev/null | grep -q "ELF"; then
chmod +x "$PTY_FILE"
echo "✅ PTY权限设置完成 ($ARCH)"
else
echo "⚠️ PTY文件无效(非ELF二进制),已删除,服务启动时将自动重新下载"
rm -f "$PTY_FILE"
fi
else
echo "ℹ️ PTY文件将在服务启动时自动下载"
fi

# 启动应用
cd server
node index.js
else
echo "❌ 未找到GSM3应用文件,正在启动传统Steam服务器管理..."
echo

# 传统的Steam服务器管理菜单
ARCH=$(uname -m)
while true; do
Expand Down