mirror of
https://github.com/GSManagerXZ/GameServerManager.git
synced 2026-05-13 17:30:22 +08:00
迁移pty目录
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,7 +27,9 @@ users.json
|
||||
favorites.json
|
||||
server/data/environment
|
||||
server/data/wallpapers/
|
||||
server/data/lib/
|
||||
data/wallpapers/
|
||||
data/lib/
|
||||
|
||||
|
||||
dist/
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -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依赖并配置最终权限
|
||||
|
||||
@@ -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
62
docs/PTY集成说明.md
Normal 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/` 目录
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -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 和贡献者们。
|
||||
@@ -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))
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
201
server/src/utils/ptyManager.ts
Normal file
201
server/src/utils/ptyManager.ts
Normal 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 }
|
||||
13
start.sh
13
start.sh
@@ -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
|
||||
|
||||
# 启动应用
|
||||
|
||||
Reference in New Issue
Block a user