迁移pty目录

This commit is contained in:
yxsj245
2026-02-27 00:45:01 +08:00
parent ba5734bff9
commit fbd63f85f4
15 changed files with 384 additions and 122 deletions

2
.gitignore vendored
View File

@@ -27,7 +27,9 @@ users.json
favorites.json
server/data/environment
server/data/wallpapers/
server/data/lib/
data/wallpapers/
data/lib/
dist/

View File

@@ -275,6 +275,19 @@ RUN mkdir -p /root/data/lib && \
"https://github.com/MCSManager/Zip-Tools/releases/latest/download/${BINARY_NAME}" && \
chmod 755 /root/data/lib/${BINARY_NAME} && \
echo "Zip-Tools 下载完成: ${BINARY_NAME}"
# 下载 PTY 二进制文件(从 GitHub Releases latest构建时预置
RUN if [ "$TARGETARCH" = "amd64" ]; then \
PTY_NAME="pty_linux_x64"; \
elif [ "$TARGETARCH" = "arm64" ]; then \
PTY_NAME="pty_linux_arm64"; \
fi && \
echo "正在下载 PTY latest (${PTY_NAME})..." && \
wget -t 3 --retry-connrefused --waitretry=2 --read-timeout=30 --timeout=15 \
-O /root/data/lib/${PTY_NAME} \
"https://github.com/MCSManager/PTY/releases/tag/latest/download/${PTY_NAME}" && \
chmod 755 /root/data/lib/${PTY_NAME} && \
echo "PTY 下载完成: ${PTY_NAME}"
# 拷贝 Python 依赖清单并安装
COPY --from=builder /app/server/src/Python/requirements.txt /tmp/requirements.txt
# 安装Python依赖并配置最终权限

View File

@@ -143,8 +143,8 @@ GSManager3/
│ │ └── Python/ # Python 脚本
│ ├── data/ # 数据存储目录
│ │ ├── games/ # 游戏数据
│ │ ├── lib/ # 运行依赖PTY、Zip-Tools 等二进制文件)
│ │ └── plugins/ # 插件数据
│ ├── PTY/ # 伪终端程序
│ └── package.json # 后端依赖
├── scripts/ # 构建脚本
├── docker-compose.yml # Docker 编排文件

62
docs/PTY集成说明.md Normal file
View File

@@ -0,0 +1,62 @@
# PTY 集成说明
## 概述
项目使用 [MCSManager/PTY](https://github.com/MCSManager/PTY) 外部二进制工具处理终端伪终端PTY会话。该工具提供跨平台的终端模拟能力。
## 自动下载机制
服务端启动时会自动检测 PTY 二进制文件是否存在:
- **已存在**:跳过下载,记录日志 `PTY 已存在,跳过下载`
- **不存在**:自动下载对应平台的二进制文件(双源策略)
- **下载失败**:记录警告日志但不阻塞服务启动,终端功能可能不可用
### 下载源优先级
1. **自建镜像(主)**`https://download.xiaozhuhouses.asia/开源项目/GSManager/GSManager3/运行依赖/PTY/`
2. **GitHub Releases备用**`https://github.com/MCSManager/PTY/releases/tag/latest/download/`
运行时优先从自建镜像下载(国内加速),失败后自动回退到 GitHub Releases。
### CI/CD 构建与打包
打包脚本(`scripts/package.js`)和 Docker 构建(`Dockerfile`)在构建时直接从 GitHub Releases 下载 PTY 并内置到产物中,确保用户部署后无需额外下载。
## 支持的平台和架构
| 操作系统 | CPU 架构 | 二进制文件名 |
|---------|---------|-------------|
| Windows | x64 | `pty_win32_x64.exe` |
| Linux | x64 | `pty_linux_x64` |
| Linux | ARM64 | `pty_linux_arm64` |
## 二进制文件存放位置
使用多路径尝试策略,按以下顺序查找:
1. `{项目根目录}/data/lib/` — 打包后环境
2. `{项目根目录}/server/data/lib/` — 开发环境
> **注意**PTY 文件已从旧的 `server/PTY/` 目录迁移到 `data/lib/` 目录,与 Zip-Tools 统一管理。
## 手动放置二进制文件(离线环境)
如果服务器无法访问外网,可以手动下载并放置二进制文件:
1. 从 [PTY Releases](https://github.com/MCSManager/PTY/releases/tag/latest) 下载对应平台的二进制文件
2. 将文件放置到 `server/data/lib/``data/lib/` 目录下
3. Linux 平台需要设置可执行权限:`chmod 755 pty_linux_x64`
## 使用的模块
- `PtyManager``server/src/utils/ptyManager.ts`)— PTY 二进制文件的路径解析、检测、下载管理
- `TerminalManager``server/src/modules/terminal/TerminalManager.ts`)— 通过 PtyManager 获取 PTY 路径并创建终端会话
## 迁移说明(从旧版本升级)
旧版本将 PTY 文件存放在 `server/PTY/` 目录下,新版本已迁移到 `data/lib/` 目录。升级后:
- 旧的 `server/PTY/` 目录可以安全删除
- 服务启动时会自动检测并下载 PTY 到新目录
- 打包产物中不再包含 `server/PTY/` 目录

View File

@@ -121,7 +121,7 @@ if test "$install_type" = "1"; then
echo "下载完毕,解压中,请稍等"
tar -xzf gsm3.tgz -C "$install_path"
rm -rf gsm3.tgz
chmod 755 "$install_path/node/bin/node" "$install_path/start.sh" "$install_path"/server/PTY/pty*
chmod 755 "$install_path/node/bin/node" "$install_path/start.sh" "$install_path"/data/lib/pty* 2>/dev/null || true
echo "SERVER_PORT=$server_port" >> "$install_path/.env"
if test "$install_to_systemd" = "yes"; then
mkdir -pv /usr/local/lib/systemd/system

View File

@@ -26,6 +26,9 @@ const nodeVersion = '22.17.0'
// Zip-Tools GitHub 下载配置(始终使用最新版本)
const ZIP_TOOLS_GITHUB_URL = 'https://github.com/MCSManager/Zip-Tools/releases/latest/download/'
// PTY GitHub 下载配置(使用 latest 标签)
const PTY_GITHUB_URL = 'https://github.com/MCSManager/PTY/releases/tag/latest/download/'
/**
* 获取目标平台对应的 Zip-Tools 二进制文件名列表
* 打包时下载所有该平台支持的架构版本
@@ -115,6 +118,60 @@ async function downloadZipTools(platform) {
console.log('✅ Zip-Tools 下载完成')
}
/**
* 获取目标平台对应的 PTY 二进制文件名列表
* 打包时下载所有该平台支持的架构版本
*/
function getPtyBinaries(platform) {
if (platform === 'linux') {
return ['pty_linux_x64', 'pty_linux_arm64']
} else if (platform === 'windows') {
return ['pty_win32_x64.exe']
}
// 未指定平台时下载所有版本
return [
'pty_linux_x64',
'pty_linux_arm64',
'pty_win32_x64.exe',
]
}
/**
* 下载 PTY 二进制文件到打包目录的 data/lib/
* 从 GitHub Releases 下载,确保打包产物内置 PTY
*/
async function downloadPty(platform) {
const binaries = getPtyBinaries(platform)
const libDir = path.join(packageDir, 'data', 'lib')
await fs.ensureDir(libDir)
console.log('📥 正在从 GitHub 下载 PTY (latest)...')
for (const binaryName of binaries) {
const url = `${PTY_GITHUB_URL}${binaryName}`
const destPath = path.join(libDir, binaryName)
console.log(` 下载: ${binaryName}`)
try {
await downloadFile(url, destPath)
// 非 Windows 二进制文件设置可执行权限
if (!binaryName.endsWith('.exe')) {
try {
execSync(`chmod +x "${destPath}"`)
} catch (e) {
// Windows 构建环境无法 chmod忽略
}
}
console.log(`${binaryName} 下载完成`)
} catch (err) {
console.error(`${binaryName} 下载失败: ${err.message}`)
throw err
}
}
console.log('✅ PTY 下载完成')
}
async function downloadNodejs(platform) {
const nodeUrls = {
linux: `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-x64.tar.xz`,
@@ -207,11 +264,7 @@ async function createPackage() {
path.join(packageDir, 'server', 'package.json')
)
// 复制PTY文件
await fs.copy(
path.join(__dirname, '..', 'server', 'PTY'),
path.join(packageDir, 'server', 'PTY')
)
// PTY 文件不再从本地复制,改为从 GitHub 下载到 data/lib/ 目录
// 复制环境变量配置文件
await fs.copy(
@@ -285,6 +338,14 @@ async function createPackage() {
console.log(' 用户启动时会自动从镜像站下载')
}
// 下载 PTY 二进制文件(从 GitHub Releases
try {
await downloadPty(buildTarget)
} catch (error) {
console.error('⚠️ PTY 下载失败,打包产物中将不包含 PTY:', error.message)
console.log(' 用户启动时会自动从镜像站下载')
}
console.log('📝 创建启动脚本...')
// 根据目标平台创建启动脚本
if (buildTarget === 'windows') {
@@ -296,7 +357,7 @@ async function createPackage() {
} else if (buildTarget === 'linux') {
const startShScript = `#!/bin/bash
echo "正在启动GSM3管理面板..."
chmod +x server/PTY/pty_linux_x64
# PTY 文件已迁移到 data/lib/ 目录,启动时由服务端自动检测
node/bin/node server/index.js`
await fs.writeFile(
@@ -326,7 +387,7 @@ pause`
const startShScript = `#!/bin/bash
echo "正在启动GSM3管理面板..."
chmod +x server/PTY/pty_linux_x64
# PTY 文件已迁移到 data/lib/ 目录,启动时由服务端自动检测
node server/index.js`
await fs.writeFile(

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,67 +0,0 @@
Pseudo-teletype App
-- -- --
仿真终端应用程序,支持运行所有 Linux/Windows 程序,可以为您的更高层应用带来完全终端控制能力。
中文 | English
terminal image
图片中表示的是,使用仿真终端运行 Minecraft 服务器,并且按下 Tab 键来选取提示。
什么是 PTY/TTY
tty = "teletype"pty = "pseudo-teletype"
众所周知程序拥有输入与输出流但是数据流与显示器之间有一个区别那便是缺少行和高的排列维度。简而言之PTY 的中文意义就是伪装设备终端,让我们的程序伪装成一个拥有固定高宽的显示器,接受来自程序的输出内容。
使用
开一个 PTY 并执行命令设置固定窗口大小IO 流直接转发。
注意:-cmd 接收的是一个数组, 命令的参数以数组的形式传递,且需要序列化,如:[\"java\",\"-jar\",\"ser.jar\",\"nogui\"]
go build
./pty -dir "." -cmd [\"bash\"] -size 50,50
接下来您会得到一个设置好大小宽度的窗口,并且您可以像 SSH 终端一样,进行任何交互。
ping google.com
top
htop
参数:
-cmd string
command
-coder string
Coder (default "UTF-8")
-dir string
command work path (default ".")
-size string
Initialize pty size, stdin will be forwarded directly (default "50,50")
-test
Test whether the system environment is pty compatible
兼容性
支持所有现代主流版本 Linux 系统。
支持 Windows 7 到 Windows 11 所有版本系统,包括 Server 系列。
支持 windows amd64 / linux amd64 & arm64。
MCSManager
MCSManager 是一款开源,分布式,开箱即用,支持 Minecraft 和其他控制台应用的程序管理面板。
这个程序是专门为了 MCSManager 而设计,您也可以尝试嵌入到您自己的程序中。
More info: https://github.com/mcsmanager
贡献
此程序属于 MCSManager 的最重要的核心功能之一,非必要不新增功能。
如果您想为这个项目提供新功能,那您必须开一个 issue 说明此功能,并提供编程思路,我们一起经过讨论后再决定是否开发
如果您是修复 BUG可以直接提交 PR 并说明情况
MIT license
遵循 MIT License 开源协议。
版权所有 zijiren233 和贡献者们。

View File

@@ -50,6 +50,7 @@ import networkRouter from './routes/network.js'
import cloudBuildRouter from './routes/cloudBuild.js'
import { consoleLogBuffer } from './utils/logger.js'
import { zipToolsManager } from './utils/zipToolsManager.js'
import { ptyManager } from './utils/ptyManager.js'
// 获取当前文件目录
const __filename = fileURLToPath(import.meta.url)
@@ -626,6 +627,15 @@ async function startServer() {
// 不阻塞启动
}
// 检测并下载 PTY
try {
await ptyManager.ensureInstalled()
logger.info('PTY 已就绪')
} catch (error: any) {
logger.warn('PTY 下载失败,终端功能可能不可用:', error.message || error)
// 不阻塞启动
}
// 设置路由
app.use('/api/auth', setupAuthRoutes(authManager))
app.use('/api/terminal', setupTerminalRoutes(terminalManager))

View File

@@ -11,6 +11,7 @@ import { promisify } from 'util'
import { exec } from 'child_process'
import { TerminalSessionManager, PersistedTerminalSession } from './TerminalSessionManager.js'
import { ConfigManager } from '../config/ConfigManager.js'
import { ptyManager } from '../../utils/ptyManager.js'
const execAsync = promisify(exec)
@@ -72,47 +73,8 @@ export class TerminalManager {
this.configManager = configManager
this.sessionManager = new TerminalSessionManager(logger)
// 根据操作系统和架构选择PTY程序路径
const platform = os.platform()
const arch = os.arch()
const baseDir = process.cwd()
let ptyFileName: string
if (platform === 'win32') {
ptyFileName = 'pty_win32_x64.exe'
} else {
// Linux平台根据架构选择对应的PTY文件
if (arch === 'arm64' || arch === 'aarch64') {
ptyFileName = 'pty_linux_arm64'
} else {
ptyFileName = 'pty_linux_x64'
}
}
// 尝试多个可能的路径来查找 PTY 文件
const possiblePaths = [
path.join(baseDir, 'PTY', ptyFileName), // 打包后的路径
path.join(baseDir, 'server', 'PTY', ptyFileName), // 开发环境路径
]
// PTY 路径将在 initialize() 中通过 ptyManager 异步获取
this.ptyPath = ''
for (const possiblePath of possiblePaths) {
try {
fs.accessSync(possiblePath, fs.constants.F_OK)
this.ptyPath = possiblePath
break
} catch {
// 继续尝试下一个路径
}
}
if (!this.ptyPath) {
this.logger.error(`无法找到 PTY 文件: ${ptyFileName}`)
// 使用默认路径作为最后的备用
this.ptyPath = path.resolve(__dirname, '../../PTY', ptyFileName)
}
this.logger.info(`终端管理器初始化完成PTY路径: ${this.ptyPath}`)
// 定期清理不活跃的会话 - 已禁用
// setInterval(() => {
@@ -127,9 +89,26 @@ export class TerminalManager {
/**
* 初始化终端管理器
* 通过 ptyManager 获取 PTY 二进制文件路径
*/
async initialize(): Promise<void> {
await this.sessionManager.initialize()
// 通过 ptyManager 获取 PTY 路径(已在启动时确保下载)
try {
this.ptyPath = await ptyManager.getPtyPath()
this.logger.info(`终端管理器初始化完成PTY路径: ${this.ptyPath}`)
} catch (error: any) {
this.logger.error(`无法找到 PTY 文件: ${error.message}`)
// 使用 ptyManager 获取平台对应的文件名作为备用路径
try {
const ptyFileName = ptyManager.getBinaryName()
this.ptyPath = path.join(process.cwd(), 'data', 'lib', ptyFileName)
this.logger.warn(`使用备用 PTY 路径: ${this.ptyPath}`)
} catch (nameError: any) {
this.logger.error(`无法获取 PTY 文件名: ${nameError.message}`)
}
}
}
/**

View File

@@ -0,0 +1,201 @@
import path from 'path'
import fs from 'fs/promises'
import { createWriteStream } from 'fs'
import { pipeline } from 'stream/promises'
import logger from './logger.js'
/**
* 支持的操作系统平台列表
*/
const SUPPORTED_PLATFORMS = new Set(['win32', 'linux'])
/**
* 支持的 CPU 架构列表
*/
const SUPPORTED_ARCHS = new Set(['x64', 'arm64'])
/**
* PTY 二进制文件管理器
* 负责 PTY 二进制文件的路径解析、检测、下载
* 参照 ZipToolsManager 的设计模式
*/
class PtyManager {
/** 自建镜像下载基础 URL运行时使用国内加速 */
private readonly DOWNLOAD_BASE_URL =
'https://download.xiaozhuhouses.asia/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/GSManager/GSManager3/%E8%BF%90%E8%A1%8C%E4%BE%9D%E8%B5%96/PTY/'
/** GitHub Releases 备用下载 URL */
private readonly FALLBACK_DOWNLOAD_URL =
'https://github.com/MCSManager/PTY/releases/tag/latest/download/'
/**
* 获取当前平台对应的 PTY 二进制文件名
* 命名规则pty_{platform}_{arch}Windows 追加 .exe
*
* 实际文件命名:
* - win32/x64 → pty_win32_x64.exe
* - linux/x64 → pty_linux_x64
* - linux/arm64 → pty_linux_arm64
*/
getBinaryName(): string {
const platform = process.platform
const arch = process.arch
if (!SUPPORTED_PLATFORMS.has(platform)) {
throw new Error(`不支持的操作系统平台: ${platform}`)
}
if (!SUPPORTED_ARCHS.has(arch)) {
throw new Error(`不支持的 CPU 架构: ${arch}`)
}
const name = `pty_${platform}_${arch}`
return platform === 'win32' ? `${name}.exe` : name
}
/**
* 获取 lib 目录的候选路径列表
* 使用多路径尝试策略,兼容打包后环境和开发环境
*/
private getLibDirCandidates(): string[] {
const baseDir = process.cwd()
return [
path.join(baseDir, 'data', 'lib'), // 打包后环境
path.join(baseDir, 'server', 'data', 'lib'), // 开发环境
]
}
/**
* 使用多路径尝试策略获取 PTY 二进制文件绝对路径
* 依次尝试 data/lib/ 和 server/data/lib/ 目录
*/
async getPtyPath(): Promise<string> {
const binaryName = this.getBinaryName()
const candidates = this.getLibDirCandidates()
for (const libDir of candidates) {
const fullPath = path.join(libDir, binaryName)
try {
await fs.access(fullPath)
return fullPath
} catch {
// 该路径不存在,尝试下一个
}
}
throw new Error(
`未找到 PTY 二进制文件 (${binaryName}),已尝试路径: ${candidates.map(d => path.join(d, binaryName)).join(', ')}`
)
}
/**
* 检测 PTY 二进制文件是否存在
*/
async isInstalled(): Promise<boolean> {
try {
await this.getPtyPath()
return true
} catch {
return false
}
}
/**
* 从指定 URL 下载二进制文件到目标路径
* 非 Windows 平台设置 chmod 0o755
*/
private async downloadFromUrl(url: string, targetPath: string): Promise<void> {
const axios = (await import('axios')).default
const response = await axios.get(url, {
responseType: 'stream',
timeout: 60000, // 60 秒超时
})
// 使用流式写入文件
const writer = createWriteStream(targetPath)
await pipeline(response.data, writer)
// 检查文件大小,防止下载空文件
const stat = await fs.stat(targetPath)
if (stat.size === 0) {
await fs.unlink(targetPath)
throw new Error('下载的文件大小为 0已删除')
}
// 非 Windows 平台设置可执行权限
if (process.platform !== 'win32') {
await fs.chmod(targetPath, 0o755)
}
}
/**
* 下载 PTY 二进制文件到第一个可写的 lib 目录
* 优先从自建镜像下载,失败后回退到 GitHub Releases
*/
async download(): Promise<void> {
const binaryName = this.getBinaryName()
const candidates = this.getLibDirCandidates()
// 选择第一个可用的 lib 目录(优先打包后路径)
let targetDir: string | null = null
for (const dir of candidates) {
try {
await fs.mkdir(dir, { recursive: true })
targetDir = dir
break
} catch {
// 无法创建该目录,尝试下一个
}
}
if (!targetDir) {
throw new Error(`无法创建 lib 目录,已尝试: ${candidates.join(', ')}`)
}
const targetPath = path.join(targetDir, binaryName)
const primaryUrl = `${this.DOWNLOAD_BASE_URL}${binaryName}`
const fallbackUrl = `${this.FALLBACK_DOWNLOAD_URL}${binaryName}`
// 优先从自建镜像下载
logger.info(`正在从自建镜像下载 PTY: ${primaryUrl}`)
try {
await this.downloadFromUrl(primaryUrl, targetPath)
logger.info(`PTY 下载完成: ${targetPath}`)
return
} catch (primaryError: any) {
logger.warn(`自建镜像下载失败: ${primaryError.message || primaryError},尝试 GitHub 备用地址...`)
// 清理可能的残留文件
try { await fs.unlink(targetPath) } catch { /* 忽略 */ }
}
// 回退到 GitHub Releases
logger.info(`正在从 GitHub 下载 PTY: ${fallbackUrl}`)
try {
await this.downloadFromUrl(fallbackUrl, targetPath)
logger.info(`PTY 下载完成GitHub 备用): ${targetPath}`)
} catch (fallbackError: any) {
// 清理可能的残留文件
try { await fs.unlink(targetPath) } catch { /* 忽略 */ }
const message = `PTY 下载失败(两个源均不可用): ${fallbackError.message || fallbackError}`
logger.error(message)
throw new Error(message)
}
}
/**
* 确保 PTY 二进制文件可用(检测 + 自动下载)
* 服务端启动时调用
*/
async ensureInstalled(): Promise<void> {
if (await this.isInstalled()) {
logger.info('PTY 已存在,跳过下载')
return
}
await this.download()
}
}
/** 导出单例实例 */
export const ptyManager = new PtyManager()
/** 导出类本身(用于测试) */
export { PtyManager }

View File

@@ -14,16 +14,17 @@ if [ -f "server/index.js" ]; then
echo "📍 默认账户: admin / admin123"
echo
# 设置PTY可执行权限(根据架构选择)
# PTY 文件已迁移到 data/lib/ 目录,启动时由服务端自动检测和下载
# 如果 data/lib/ 中存在 PTY 文件,设置可执行权限
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ] && [ -f "server/PTY/pty_linux_x64" ]; then
chmod +x server/PTY/pty_linux_x64
if [ "$ARCH" = "x86_64" ] && [ -f "data/lib/pty_linux_x64" ]; then
chmod +x data/lib/pty_linux_x64
echo "✅ PTY权限设置完成 (x64)"
elif [ "$ARCH" = "aarch64" ] && [ -f "server/PTY/pty_linux_arm64" ]; then
chmod +x server/PTY/pty_linux_arm64
elif [ "$ARCH" = "aarch64" ] && [ -f "data/lib/pty_linux_arm64" ]; then
chmod +x data/lib/pty_linux_arm64
echo "✅ PTY权限设置完成 (arm64)"
else
echo " 未找到对应架构的PTY文件使用默认配置"
echo " PTY文件将在服务启动时自动下载"
fi
# 启动应用