diff --git a/README.md b/README.md index 46e5476..4964e6c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,67 @@ # VHD Mounter - 智能SEGA街机游戏VHD挂载管理和运行保活工具 +> 🎮 **完整的街机游戏VHD管理解决方案** +> 包含本地VHD挂载工具和配套的Web服务器,提供集中化配置管理和远程控制功能 + +## 📦 项目组成 + +本项目包含两个核心组件: + +### 🖥️ VHD Mounter (主程序) +智能VHD挂载和进程保活工具,支持本地和远程配置管理 + +### 🌐 VHDSelectServer (配套服务器) +基于Node.js的Web服务器,提供: +- 🎯 **集中化VHD选择管理** - Web GUI界面和RESTful API +- 🐳 **Docker化部署** - 支持内置/外部PostgreSQL数据库 +- 🔐 **用户认证系统** - 安全的会话管理 +- 🛡️ **机台保护功能** - 防止误操作的保护机制 +- 📊 **实时状态监控** - 健康检查和系统状态API + +## 📁 项目结构 + +``` +vhdmount/ +├── 📁 VHDSelectServer/ # Web服务器子项目 +│ ├── 📄 server.js # 主服务器文件 +│ ├── 📄 database.js # 数据库操作模块 +│ ├── 📄 package.json # Node.js依赖配置 +│ ├── 📄 Dockerfile # Docker镜像构建文件 +│ ├── 📄 docker-compose.yml # Docker Compose配置 +│ ├── 📄 docker-compose.external-db.yml # 外部数据库配置 +│ ├── 📄 docker-entrypoint.sh # Docker启动脚本 +│ ├── 📄 init-db.sql # 数据库初始化脚本 +│ ├── 📄 init-embedded-db.sh # 内置数据库初始化 +│ ├── 📄 setup_postgresql.sql # PostgreSQL设置脚本 +│ ├── 📄 README.md # VHDSelectServer详细文档 +│ ├── 📄 DOCKER_TROUBLESHOOTING.md # Docker故障排除指南 +│ ├── 📄 POSTGRESQL_SETUP.md # PostgreSQL设置指南 +│ └── 📁 config/ # 配置文件目录 +├── 📄 VHDMounter.exe # 主程序可执行文件 +├── 📄 VHDMounter.csproj # .NET项目文件 +├── 📄 MainWindow.xaml # WPF主窗口界面 +├── 📄 MainWindow.xaml.cs # 主窗口逻辑代码 +├── 📄 VHDManager.cs # VHD管理核心类 +├── 📄 Program.cs # 程序入口点 +├── 📄 vhdmonter_config.ini # 客户端配置文件 +├── 📄 README.md # 项目主文档 +├── 📄 VHD_Mount_Fix_Guide.md # VHD挂载故障排除指南 +├── 📄 build.bat # 构建脚本 +├── 📄 debug.bat # 调试脚本 +├── 📄 run_as_admin.bat # 管理员运行脚本 +└── 📁 .github/workflows/ # GitHub Actions CI/CD配置 + └── 📄 build.yml # 自动构建配置 +``` + +### 目录说明 + +- **VHDSelectServer/**: 独立的Web服务器子项目,提供集中化配置管理 +- **主程序文件**: VHD挂载和进程管理的核心Windows应用程序 +- **配置文件**: 客户端和服务器的配置管理文件 +- **文档**: 详细的使用指南和故障排除文档 +- **脚本**: 构建、调试和部署的辅助脚本 +- **CI/CD**: 自动化构建和部署配置 + ## ✨ 核心特性 ### 🔍 智能扫描与识别 @@ -8,10 +70,19 @@ - **快速定位**:优化的扫描算法,快速定位目标文件 ### 🌐 智能网络配置管理 -- **远程关键词获取**:通过HTTP API自动获取启动关键词,实现集中化配置管理 +- **VHDSelectServer集成**:配套的Node.js Web服务器,提供完整的集中化配置管理解决方案 +- **RESTful API接口**:标准化的HTTP API,支持VHD关键词获取、设置和状态查询 +- **Web GUI管理界面**:直观的网页操作界面,支持实时VHD选择和机台保护设置 +- **多部署模式支持**: + - Docker单容器部署(内置PostgreSQL数据库) + - Docker Compose部署(外部PostgreSQL数据库) + - 本地开发模式部署 - **配置文件驱动**:通过`vhdmonter_config.ini`灵活控制远程功能开关和服务器地址 - **智能降级机制**:远程获取失败时自动回退到本地选择模式,确保程序稳定运行 -- **跨平台服务器支持**:配套VHDSelectServer提供Web GUI和API接口,支持Docker部署 +- **实时配置同步**:服务器端配置变更立即生效,无需重启客户端程序 +- **安全认证机制**:基于会话的用户认证,防止未授权访问和误操作 +- **机台保护功能**:支持设置机台保护状态,防止意外的VHD切换操作 +- **健康检查监控**:内置健康检查端点,支持服务状态监控和故障诊断 ### 🔌 智能USB设备管理 - **NX_INS设备识别**:自动检测卷标为"NX_INS"的USB设备,专为街机系统优化 @@ -41,6 +112,121 @@ - **安全退出**:确保在系统关机前完成VHD卸载操作 - **资源保护**:防止因异常退出导致的VHD资源占用 +## 🌐 VHDSelectServer - 集中化配置管理服务器 + +VHDSelectServer是本项目的配套Web服务器,提供集中化的VHD选择管理和远程控制功能。 + +### ✨ 主要特性 + +#### 🎯 集中化VHD管理 +- **Web GUI界面**:直观的网页操作界面,支持VHD关键词选择 +- **RESTful API**:完整的API接口,支持程序化集成 +- **实时配置**:动态更新VHD选择配置,无需重启客户端 + +#### 🐳 Docker化部署 +- **一键部署**:支持Docker和Docker Compose快速部署 +- **双数据库模式**: + - 内置PostgreSQL:单容器部署,适合小规模使用 + - 外部PostgreSQL:分离式部署,适合生产环境 +- **健康检查**:内置健康检查机制,确保服务稳定运行 + +#### 🔐 安全认证系统 +- **用户登录**:基于会话的用户认证机制 +- **权限控制**:防止未授权访问和误操作 +- **会话管理**:安全的会话超时和自动登出 + +#### 🛡️ 机台保护功能 +- **保护状态**:可设置机台保护状态,防止误操作 +- **状态持久化**:保护状态自动保存到数据库 +- **API控制**:支持通过API查询和设置保护状态 + +#### 📊 系统监控 +- **状态API**:实时系统状态查询接口 +- **健康检查**:服务健康状态监控 +- **版本信息**:API版本和系统信息查询 + +### 🚀 快速部署 + +#### Docker部署(推荐) + +**方式一:内置数据库(单容器)** +```bash +# 构建镜像 +docker build -t vhd-select-server ./VHDSelectServer + +# 运行容器 +docker run -d \ + --name vhd-select-server \ + -p 8080:8080 \ + -v vhd_data:/app/data \ + vhd-select-server +``` + +**方式二:外部数据库(Docker Compose)** +```bash +# 进入VHDSelectServer目录 +cd VHDSelectServer + +# 启动服务(包含PostgreSQL) +docker-compose up -d + +# 查看服务状态 +docker-compose ps +``` + +#### 本地开发部署 +```bash +# 进入VHDSelectServer目录 +cd VHDSelectServer + +# 安装依赖 +npm install + +# 启动服务 +npm start +``` + +### 🔧 配置说明 + +#### 环境变量配置 +- `DB_TYPE`:数据库类型(embedded/external) +- `DB_HOST`:数据库主机地址(外部数据库模式) +- `DB_PORT`:数据库端口(默认5432) +- `DB_NAME`:数据库名称(默认vhd_select) +- `DB_USER`:数据库用户名(默认postgres) +- `DB_PASSWORD`:数据库密码(默认postgres) +- `PORT`:服务端口(默认8080) + +#### 客户端配置 +在主程序目录下的`vhdmonter_config.ini`文件中配置: +```ini +[RemoteSelection] +EnableRemoteSelection=true +BootImageSelectUrl=http://localhost:8080/api/boot-image-select +``` + +### 📡 API接口 + +详细的API文档请参考 + +主要接口包括: +- `GET /api/boot-image-select` - 获取当前VHD选择 +- `POST /api/set-vhd` - 设置VHD关键词 +- `GET /api/status` - 获取系统状态 +- `POST /api/auth/login` - 用户登录 +- `GET /api/machines` - 机台管理 +- `POST /api/protect` - 设置保护状态 + +### 🌍 Web界面 + +访问 `http://localhost:8080` 打开Web管理界面,功能包括: +- VHD关键词选择和设置 +- 机台保护状态管理 +- 系统状态监控 +- 用户登录和会话管理 + +默认登录密码:`admin123` + ## 🚀 快速开始 ### 系统要求 @@ -52,12 +238,14 @@ ### 安装与运行 -#### 方式一:直接运行(推荐) +#### VHD Mounter (主程序) + +**方式一:直接运行(推荐)** 1. 下载最新版本的可执行文件 2. 右键点击 → "以管理员身份运行" 3. 程序将自动开始扫描和挂载流程 -#### 方式二:从源码编译 +**方式二:从源码编译** ```powershell # 克隆仓库 git clone https://github.com/username/vhdmount.git @@ -70,6 +258,41 @@ dotnet build --configuration Release dotnet publish --configuration Release --self-contained true --runtime win-x64 ``` +#### VHDSelectServer (配套服务器) + +**Docker部署(推荐)** +```powershell +# 进入VHDSelectServer目录 +cd VHDSelectServer + +# 方式一:单容器部署(内置数据库) +docker build -t vhd-select-server . +docker run -d --name vhd-select-server -p 8080:8080 -v vhd_data:/app/data vhd-select-server + +# 方式二:Docker Compose部署(外部数据库) +docker-compose up -d +``` + +**本地开发部署** +```powershell +# 进入VHDSelectServer目录 +cd VHDSelectServer + +# 安装Node.js依赖 +npm install + +# 启动服务器 +npm start +``` + +**配置客户端连接** +在主程序目录创建或编辑 `vhdmonter_config.ini`: +```ini +[RemoteSelection] +EnableRemoteSelection=true +BootImageSelectUrl=http://localhost:8080/api/boot-image-select +``` + ## 📖 使用指南 ### 基本操作流程 @@ -151,12 +374,43 @@ VHDMounter.exe --debug ## 🔧 技术架构 +### 整体架构 + +本项目采用客户端-服务器架构,包含两个核心组件: + +``` +┌─────────────────────┐ HTTP API ┌─────────────────────┐ +│ VHD Mounter │◄──────────────►│ VHDSelectServer │ +│ (Windows Client) │ │ (Web Server) │ +└─────────────────────┘ └─────────────────────┘ +│ │ +├─ VHD挂载管理 ├─ 集中化配置管理 +├─ 进程监控保活 ├─ Web GUI界面 +├─ USB设备检测 ├─ RESTful API +├─ 远程配置获取 ├─ 用户认证系统 +└─ 本地文件操作 └─ 数据库持久化 +``` + ### 核心技术栈 + +#### VHD Mounter (客户端) - **UI框架**:WPF (.NET 6.0) - **VHD操作**:Windows DiskPart API - **进程管理**:System.Diagnostics - **系统集成**:Microsoft.Win32.SystemEvents - **异步编程**:async/await 模式 +- **HTTP客户端**:HttpClient (.NET 6.0) +- **配置管理**:INI文件解析 + +#### VHDSelectServer (服务端) +- **运行时**:Node.js 18+ +- **Web框架**:Express.js +- **数据库**:PostgreSQL 13+ +- **ORM**:原生SQL查询 +- **认证**:Express-session +- **容器化**:Docker & Docker Compose +- **前端**:原生HTML/CSS/JavaScript +- **健康检查**:内置健康检查端点 ### 关键组件 @@ -181,6 +435,46 @@ VHDMounter.exe --debug - 应用程序生命周期控制 - 执行步骤状态显示和暂停控制 +#### VHDSelectServer 核心模块 + +**server.js (主服务器)** +- Express应用初始化和路由配置 +- 中间件管理(认证、CORS、静态文件) +- API端点实现和错误处理 +- 服务器启动和健康检查 + +**database.js (数据库层)** +- PostgreSQL连接管理 +- 数据库初始化和表创建 +- SQL查询封装和事务处理 +- 连接池管理和错误恢复 + +**认证中间件** +- 会话验证和用户状态检查 +- 登录状态管理和权限控制 +- 安全头设置和CSRF防护 + +**API路由模块** +- `/api/auth/*` - 用户认证相关接口 +- `/api/boot-image-select` - VHD选择获取接口 +- `/api/set-vhd` - VHD关键词设置接口 +- `/api/machines/*` - 机台管理接口 +- `/api/protect` - 保护状态控制接口 +- `/api/status` - 系统状态查询接口 + +### 集成机制 + +#### 客户端-服务器通信 +- **HTTP协议**:基于RESTful API的标准HTTP通信 +- **JSON格式**:统一的数据交换格式 +- **错误处理**:完整的错误响应和降级机制 +- **超时控制**:网络请求超时和重试机制 + +#### 配置同步 +- **实时更新**:服务器配置变更立即生效 +- **本地缓存**:客户端配置文件作为备用方案 +- **降级策略**:网络异常时自动切换到本地模式 + ### 安全机制 - **权限检查**:启动时验证管理员权限 - **资源清理**:异常情况下的资源自动释放 diff --git a/VHDManager.cs b/VHDManager.cs index 8d3d3e3..6813e5e 100644 --- a/VHDManager.cs +++ b/VHDManager.cs @@ -9,10 +9,11 @@ using System.Text.RegularExpressions; using System.Net.Http; using System.Text.Json; using System.Text; +using System.Timers; namespace VHDMounter { - public class VHDManager + public class VHDManager : IDisposable { private const string TARGET_DRIVE = "M:"; private readonly string[] TARGET_KEYWORDS = { "SDEZ", "SDHD", "SDDT" }; @@ -23,6 +24,146 @@ namespace VHDMounter private static readonly HttpClient httpClient = new HttpClient(); private string configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "vhdmonter_config.ini"); + // 保护检查相关字段 + private Timer protectionTimer; + private string machineId; + private bool isProtectionCheckEnabled; + private string protectionCheckUrl; + private int protectionCheckInterval; + + // 构造函数 + public VHDManager() + { + InitializeProtectionCheck(); + } + + // 初始化保护检查功能 + private void InitializeProtectionCheck() + { + try + { + var config = ReadConfig(); + + // 读取Machine ID + machineId = config.TryGetValue("MachineId", out var id) ? id : "MACHINE_001"; + + // 读取保护检查配置 + isProtectionCheckEnabled = config.TryGetValue("EnableProtectionCheck", out var enableCheck) && + bool.TryParse(enableCheck, out var enabled) && enabled; + + protectionCheckUrl = config.TryGetValue("ProtectionCheckUrl", out var url) ? url : "http://localhost:8080/api/protect"; + + protectionCheckInterval = config.TryGetValue("ProtectionCheckInterval", out var interval) && + int.TryParse(interval, out var intervalMs) ? intervalMs : 500; + + if (isProtectionCheckEnabled) + { + StartProtectionCheck(); + StatusChanged?.Invoke($"保护检查已启用 - Machine ID: {machineId}, 检查间隔: {protectionCheckInterval}ms"); + } + else + { + StatusChanged?.Invoke("保护检查功能已禁用"); + } + } + catch (Exception ex) + { + StatusChanged?.Invoke($"初始化保护检查失败: {ex.Message}"); + } + } + + // 启动保护检查定时器 + private void StartProtectionCheck() + { + if (protectionTimer != null) + { + protectionTimer.Stop(); + protectionTimer.Dispose(); + } + + protectionTimer = new Timer(protectionCheckInterval); + protectionTimer.Elapsed += async (sender, e) => await CheckProtectionStatus(); + protectionTimer.AutoReset = true; + protectionTimer.Start(); + } + + // 检查保护状态 + private async Task CheckProtectionStatus() + { + try + { + if (!isProtectionCheckEnabled || string.IsNullOrWhiteSpace(protectionCheckUrl)) + return; + + var requestUrl = $"{protectionCheckUrl}?machineId={Uri.EscapeDataString(machineId)}"; + var response = await httpClient.GetAsync(requestUrl); + + if (response.IsSuccessStatusCode) + { + var jsonContent = await response.Content.ReadAsStringAsync(); + var jsonDoc = JsonDocument.Parse(jsonContent); + + if (jsonDoc.RootElement.TryGetProperty("protected", out var protectedElement) && + protectedElement.GetBoolean()) + { + StatusChanged?.Invoke($"收到保护信号 - Machine ID: {machineId},正在执行自动关机..."); + await ExecuteShutdown(); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"保护状态检查失败: {ex.Message}"); + } + } + + // 执行自动关机 + private async Task ExecuteShutdown() + { + try + { + StatusChanged?.Invoke("正在执行自动关机..."); + + // 停止保护检查定时器 + if (protectionTimer != null) + { + protectionTimer.Stop(); + protectionTimer.Dispose(); + protectionTimer = null; + } + + // 执行关机命令 + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/s /t 0", + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + await process.WaitForExitAsync(); + } + catch (Exception ex) + { + StatusChanged?.Invoke($"自动关机执行失败: {ex.Message}"); + } + } + + // 停止保护检查 + public void StopProtectionCheck() + { + if (protectionTimer != null) + { + protectionTimer.Stop(); + protectionTimer.Dispose(); + protectionTimer = null; + } + } + // 调试方法:检查特定文件是否符合条件 public bool IsVHDFileValid(string filePath) { @@ -402,9 +543,11 @@ namespace VHDMounter return null; } - StatusChanged?.Invoke($"正在从远程获取VHD选择: {url}"); + // 添加Machine ID参数 + var requestUrl = $"{url}?machineId={Uri.EscapeDataString(machineId)}"; + StatusChanged?.Invoke($"正在从远程获取VHD选择: {requestUrl}"); - var response = await httpClient.GetAsync(url); + var response = await httpClient.GetAsync(requestUrl); if (!response.IsSuccessStatusCode) { StatusChanged?.Invoke($"远程请求失败: {response.StatusCode}"); @@ -417,7 +560,7 @@ namespace VHDMounter if (jsonDoc.RootElement.TryGetProperty("BootImageSelected", out var bootImageElement)) { var selectedKeyword = bootImageElement.GetString(); - StatusChanged?.Invoke($"远程选择的VHD关键词: {selectedKeyword}"); + StatusChanged?.Invoke($"远程选择的VHD关键词: {selectedKeyword} (Machine ID: {machineId})"); return selectedKeyword; } else @@ -1132,5 +1275,18 @@ exit"; return false; } } + + // IDisposable实现 + public void Dispose() + { + StopProtectionCheck(); + httpClient?.Dispose(); + } + + // 析构函数 + ~VHDManager() + { + Dispose(); + } } } \ No newline at end of file diff --git a/VHDSelectServer/.gitignore b/VHDSelectServer/.gitignore index ac9e5e9..95688cd 100644 --- a/VHDSelectServer/.gitignore +++ b/VHDSelectServer/.gitignore @@ -113,4 +113,5 @@ public # Temporary folders tmp/ -temp/ \ No newline at end of file +temp/ +vhd-data \ No newline at end of file diff --git a/VHDSelectServer/Dockerfile b/VHDSelectServer/Dockerfile index 7725c64..7af42ee 100644 --- a/VHDSelectServer/Dockerfile +++ b/VHDSelectServer/Dockerfile @@ -1,6 +1,15 @@ # 使用官方Node.js运行时作为基础镜像 FROM node:18-alpine +# 安装PostgreSQL和相关工具 +RUN apk add --no-cache \ + postgresql \ + postgresql-contrib \ + postgresql-client \ + su-exec \ + bash \ + curl + # 设置工作目录 WORKDIR /app @@ -13,8 +22,13 @@ RUN npm ci --only=production # 复制应用源代码 COPY . . -# 创建配置文件目录 -RUN mkdir -p /app/config +# 创建必要的目录 +RUN mkdir -p /app/config /var/lib/postgresql/data /app/scripts + +# 复制数据库初始化脚本 +COPY init-db.sql /app/scripts/ +COPY docker-entrypoint.sh /app/scripts/ +RUN chmod +x /app/scripts/docker-entrypoint.sh # 暴露端口 EXPOSE 8080 @@ -23,14 +37,28 @@ EXPOSE 8080 ENV NODE_ENV=production ENV PORT=8080 ENV CONFIG_PATH=/app/config +# 数据库配置环境变量 +ENV USE_EMBEDDED_DB=true +ENV DB_HOST=localhost +ENV DB_PORT=5432 +ENV DB_NAME=vhd_select +ENV DB_USER=vhd_user +ENV DB_PASSWORD=vhd_password +ENV POSTGRES_DATA_DIR=/var/lib/postgresql/data -# 创建非root用户 -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nodejs -u 1001 +# 创建postgres用户和nodejs用户 +RUN addgroup -S postgres || true && \ + adduser -S -D -H -h /var/lib/postgresql -s /bin/sh -G postgres postgres || true && \ + addgroup -S nodejs || true && \ + adduser -S nodejs -G nodejs || true -# 更改文件所有权 +# 创建PostgreSQL数据目录和运行时目录并设置权限 +RUN mkdir -p /var/lib/postgresql/data /run/postgresql && \ + chown -R postgres:postgres /var/lib/postgresql /run/postgresql && \ + chmod 700 /var/lib/postgresql/data + +# 更改应用文件所有权 RUN chown -R nodejs:nodejs /app -USER nodejs -# 启动应用 -CMD ["npm", "start"] \ No newline at end of file +# 启动脚本 +CMD ["/app/scripts/docker-entrypoint.sh"] \ No newline at end of file diff --git a/VHDSelectServer/POSTGRESQL_SETUP.md b/VHDSelectServer/POSTGRESQL_SETUP.md new file mode 100644 index 0000000..6924c6a --- /dev/null +++ b/VHDSelectServer/POSTGRESQL_SETUP.md @@ -0,0 +1,213 @@ +# PostgreSQL安装和配置指南 + +## 📋 概述 + +本指南将帮助你在Windows系统上安装和配置PostgreSQL数据库,以支持VHDSelectServer的数据持久化功能。 + +## 🔧 安装PostgreSQL + +### 方法一:官方安装程序(推荐) + +1. **下载PostgreSQL** + - 访问:https://www.postgresql.org/download/windows/ + - 点击"Download the installer" + - 选择最新版本(推荐PostgreSQL 15或16) + +2. **运行安装程序** + - 以管理员身份运行下载的安装程序 + - 按照以下设置进行安装: + + ``` + 安装组件: + ✅ PostgreSQL Server + ✅ pgAdmin 4 + ✅ Stack Builder + ✅ Command Line Tools + + 安装目录:C:\Program Files\PostgreSQL\16 (默认) + 数据目录:C:\Program Files\PostgreSQL\16\data (默认) + + 超级用户密码:password + 端口:5432 + 区域设置:[Default locale] + ``` + +3. **完成安装** + - 安装完成后,确保PostgreSQL服务已启动 + - 可以在"服务"中查看"postgresql-x64-16"服务状态 + +### 方法二:使用包管理器 + +如果你使用Chocolatey: +```bash +choco install postgresql +``` + +如果你使用Scoop: +```bash +scoop install postgresql +``` + +## 🗄️ 配置数据库 + +### 自动配置(推荐) + +1. **运行配置脚本** + ```bash + cd VHDSelectServer + setup_database.bat + ``` + +2. **输入密码** + - 脚本会提示输入PostgreSQL超级用户密码 + - 输入安装时设置的密码(默认:password) + +### 手动配置 + +如果自动配置失败,可以手动执行以下步骤: + +1. **打开命令提示符** + ```bash + # 设置PostgreSQL环境变量(如果需要) + set PATH=%PATH%;C:\Program Files\PostgreSQL\16\bin + ``` + +2. **连接到PostgreSQL** + ```bash + psql -U postgres -h localhost + ``` + +3. **执行配置脚本** + ```sql + \i setup_postgresql.sql + ``` + +## 🔍 验证安装 + +### 检查PostgreSQL服务 + +1. **检查服务状态** + ```bash + sc query postgresql-x64-16 + ``` + +2. **启动服务(如果未运行)** + ```bash + net start postgresql-x64-16 + ``` + +### 测试数据库连接 + +1. **使用psql连接** + ```bash + psql -U postgres -h localhost -d vhd_select + ``` + +2. **查看表结构** + ```sql + \dt + SELECT * FROM machines; + ``` + +### 测试VHDSelectServer连接 + +1. **更新.env文件** + 确保`.env`文件中的配置正确: + ```env + DB_HOST=localhost + DB_PORT=5432 + DB_NAME=vhd_select + DB_USER=postgres + DB_PASSWORD=password + ``` + +2. **重启VHDSelectServer** + ```bash + node server.js + ``` + +3. **检查连接状态** + - 查看控制台输出,应该显示"数据库连接成功" + - 访问Web UI:http://localhost:8080 + +## 🛠️ 故障排除 + +### 常见问题 + +1. **"psql不是内部或外部命令"** + - 解决方案:将PostgreSQL的bin目录添加到系统PATH环境变量 + - 路径:`C:\Program Files\PostgreSQL\16\bin` + +2. **连接被拒绝** + - 检查PostgreSQL服务是否运行 + - 检查防火墙设置 + - 确认端口5432未被占用 + +3. **密码认证失败** + - 确认超级用户密码正确 + - 检查pg_hba.conf配置文件 + +4. **数据库不存在** + - 重新运行setup_postgresql.sql脚本 + - 手动创建数据库:`CREATE DATABASE vhd_select;` + +### 重置数据库 + +如果需要重置数据库: + +```sql +-- 连接到postgres数据库 +\c postgres + +-- 删除现有数据库 +DROP DATABASE IF EXISTS vhd_select; + +-- 重新运行配置脚本 +\i setup_postgresql.sql +``` + +## 📊 数据库结构 + +### machines表结构 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | SERIAL | 主键,自增ID | +| machine_id | VARCHAR(255) | 机台唯一标识符 | +| protected | BOOLEAN | 保护状态 | +| vhd_keyword | VARCHAR(50) | VHD关键词 | +| created_at | TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | 更新时间 | + +### 索引 + +- `idx_machines_machine_id`: machine_id字段索引 +- `idx_machines_protected`: protected字段索引 + +## 🔐 安全建议 + +1. **修改默认密码** + - 生产环境中应修改默认的postgres用户密码 + +2. **创建专用用户** + ```sql + CREATE USER vhd_user WITH PASSWORD 'secure_password'; + GRANT ALL PRIVILEGES ON DATABASE vhd_select TO vhd_user; + ``` + +3. **配置防火墙** + - 限制PostgreSQL端口的访问权限 + +4. **定期备份** + ```bash + pg_dump -U postgres vhd_select > backup.sql + ``` + +## 📞 获取帮助 + +如果遇到问题,可以: + +1. 查看PostgreSQL日志文件 +2. 检查VHDSelectServer控制台输出 +3. 使用pgAdmin 4图形界面管理数据库 +4. 参考PostgreSQL官方文档:https://www.postgresql.org/docs/ \ No newline at end of file diff --git a/VHDSelectServer/README.md b/VHDSelectServer/README.md index 2cf1ab3..55a1605 100644 --- a/VHDSelectServer/README.md +++ b/VHDSelectServer/README.md @@ -27,37 +27,37 @@ ### 方法1: Docker部署(推荐) -1. **一键部署脚本(最简单)** - ```bash - # Windows - deploy.bat - - # Linux/macOS - chmod +x deploy.sh - ./deploy.sh - ``` +#### 选项1: 使用内置数据库(默认) +```bash +# 使用Docker Compose启动(内置PostgreSQL数据库) +docker-compose up -d -2. **使用Docker Compose** - ```bash - # 构建并启动服务 - docker-compose up -d - - # 查看日志 - docker-compose logs -f - - # 停止服务 - docker-compose down - ``` +# 查看日志 +docker-compose logs -f -3. **使用Docker命令** - ```bash - # 运行容器(配置持久化) - docker run -d \ - --name vhd-select-server \ - -p 8080:8080 \ - -v $(pwd)/config:/app/config \ - lty271104/vhd-select-server:latest - ``` +# 停止服务 +docker-compose down +``` + +#### 选项2: 使用外部数据库 +```bash +# 使用外部数据库配置文件 +docker-compose -f docker-compose.external-db.yml up -d + +# 或者手动配置环境变量 +docker run -d \ + --name vhd-select-server \ + -p 8080:8080 \ + -v $(pwd)/config:/app/config \ + -v $(pwd)/vhd-data:/app/vhd-data \ + -e USE_EMBEDDED_DB=false \ + -e DB_HOST=your-db-host \ + -e DB_PORT=5432 \ + -e DB_NAME=vhd_select \ + -e DB_USER=your-db-user \ + -e DB_PASSWORD=your-db-password \ + lty271104/vhd-select-server:latest +``` ### 方法2: Node.js版本 @@ -75,15 +75,6 @@ npm start ``` -### 方法3: Python版本 - -```bash -# 直接运行Python服务器 -python server.py - -# 或指定端口 -python server.py 8080 -``` ## 🌐 访问界面 @@ -94,24 +85,78 @@ python server.py 8080 ## 📡 API接口 -### 获取当前VHD关键词 +VHD Select Server 提供完整的RESTful API接口,支持机台管理、VHD关键词设置和状态监控。 + +### 🔐 认证接口 + +#### 登录 ```http -GET /api/boot-image-select +POST /api/auth/login +Content-Type: application/json + +{ + "password": "admin123" +} ``` **响应示例**: ```json { "success": true, - "BootImageSelected": "SDEZ", - "timestamp": "2025-10-19T03:05:33.486615" + "message": "登录成功" } ``` -### 设置VHD关键词 +#### 登出 +```http +POST /api/auth/logout +``` + +**响应示例**: +```json +{ + "success": true, + "message": "已登出" +} +``` + +#### 检查认证状态 +```http +GET /api/auth/check +``` + +**响应示例**: +```json +{ + "isAuthenticated": true +} +``` + +### 🖥️ VHD关键词接口 + +#### 获取当前VHD关键词 +```http +GET /api/boot-image-select?machineId=MACHINE001 +``` + +**参数说明**: +- `machineId` (必需): 机台唯一标识符 + +**响应示例**: +```json +{ + "success": true, + "BootImageSelected": "SDEZ", + "machineId": "MACHINE001", + "timestamp": "2025-10-21T11:39:43.809Z" +} +``` + +#### 设置全局VHD关键词 🔒 ```http POST /api/set-vhd Content-Type: application/json +Authorization: 需要登录 { "BootImageSelected": "NEW_KEYWORD" @@ -127,7 +172,113 @@ Content-Type: application/json } ``` -### 服务器状态 +### 🛡️ 机台保护接口 + +#### 获取机台保护状态 +```http +GET /api/protect?machineId=MACHINE001 +``` + +**参数说明**: +- `machineId` (必需): 机台唯一标识符 + +**响应示例**: +```json +{ + "success": true, + "protected": false, + "machineId": "MACHINE001", + "timestamp": "2025-10-21T11:39:43.809Z" +} +``` + +#### 设置机台保护状态 🔒 +```http +POST /api/protect +Content-Type: application/json +Authorization: 需要登录 + +{ + "machineId": "MACHINE001", + "protected": true +} +``` + +**响应示例**: +```json +{ + "success": true, + "protected": true, + "machineId": "MACHINE001", + "message": "机台保护状态已更新" +} +``` + +### 🏭 机台管理接口 + +#### 获取所有机台信息 🔒 +```http +GET /api/machines +Authorization: 需要登录 +``` + +**响应示例**: +```json +{ + "success": true, + "machines": [ + { + "machine_id": "MACHINE001", + "protected": false, + "vhd_keyword": "SDEZ", + "last_seen": "2025-10-21T11:39:43.809Z", + "created_at": "2025-10-21T10:00:00.000Z" + } + ], + "count": 1, + "timestamp": "2025-10-21T11:39:43.809Z" +} +``` + +#### 设置特定机台的VHD关键词 🔒 +```http +POST /api/machines/{machineId}/vhd +Content-Type: application/json +Authorization: 需要登录 + +{ + "vhdKeyword": "CUSTOM_VHD" +} +``` + +**响应示例**: +```json +{ + "success": true, + "machineId": "MACHINE001", + "vhdKeyword": "CUSTOM_VHD", + "message": "机台VHD关键词已更新" +} +``` + +#### 删除机台 🔒 +```http +DELETE /api/machines/{machineId} +Authorization: 需要登录 +``` + +**响应示例**: +```json +{ + "success": true, + "machineId": "MACHINE001", + "message": "机台已删除" +} +``` + +### 📊 系统状态接口 + +#### 获取服务器状态 ```http GET /api/status ``` @@ -138,11 +289,35 @@ GET /api/status "success": true, "status": "running", "BootImageSelected": "SDEZ", - "timestamp": "2025-10-19T03:05:33.486615", - "version": "1.0.0" + "uptime": 30.415774368, + "timestamp": "2025-10-21T11:39:43.809Z", + "version": "1.2.0" } ``` +### 📝 API使用说明 + +**认证要求**: +- 🔒 标记的接口需要先通过 `/api/auth/login` 登录 +- 登录后会话有效期为24小时 +- 默认管理员密码: `admin123` + +**错误响应格式**: +```json +{ + "success": false, + "error": "错误描述", + "requireAuth": true // 仅在需要认证时出现 +} +``` + +**状态码说明**: +- `200`: 请求成功 +- `400`: 请求参数错误 +- `401`: 未认证或认证失败 +- `404`: 资源不存在 +- `500`: 服务器内部错误 + ## 🎯 使用说明 ### Web界面操作 @@ -160,23 +335,83 @@ GET /api/status ### Docker相关 +#### 数据库配置选项 + +**内置数据库模式(默认)** +- 自动在容器内启动PostgreSQL数据库 +- 数据持久化到Docker卷 +- 零配置,开箱即用 + +**外部数据库模式** +- 连接到外部PostgreSQL数据库 +- 支持云数据库服务 +- 更好的扩展性和可维护性 + #### 环境变量 + +**应用配置** - `PORT`: 服务端口(默认: 8080) - `CONFIG_PATH`: 配置文件目录路径(默认: /app/config) - `NODE_ENV`: 运行环境(默认: production) +**数据库配置** +- `USE_EMBEDDED_DB`: 是否使用内置数据库(默认: true) +- `DB_HOST`: 数据库主机地址(默认: localhost) +- `DB_PORT`: 数据库端口(默认: 5432) +- `DB_NAME`: 数据库名称(默认: vhd_select) +- `DB_USER`: 数据库用户名(默认: postgres) +- `DB_PASSWORD`: 数据库密码(默认: vhd_select_password) +- `DB_MAX_CONNECTIONS`: 最大连接数(默认: 20) +- `DB_IDLE_TIMEOUT`: 空闲连接超时时间(默认: 30000ms) +- `DB_CONNECTION_TIMEOUT`: 连接超时时间(默认: 5000ms) +- `DB_SSL`: 是否启用SSL连接(默认: false) + #### 数据卷 - `/app/config`: 配置文件持久化目录 -- `/app/data`: 数据文件持久化目录(可选) +- `/app/vhd-data`: VHD数据文件持久化目录 +- `/var/lib/postgresql/data`: 内置数据库数据持久化目录 #### 健康检查 -- 端点: `http://localhost:8080/api/status` +- 端点: `http://localhost:8080/api/health` - 间隔: 30秒 - 超时: 10秒 - 重试: 3次 +- 启动等待时间: 60秒 + +#### 配置文件说明 + +**docker-compose.yml**: 使用内置数据库的默认配置 +**docker-compose.external-db.yml**: 使用外部数据库的示例配置 #### 故障排除 -如果Docker部署遇到问题,请参考 [Docker故障排除指南](DOCKER_TROUBLESHOOTING.md) + +**内置数据库问题** +```bash +# 检查数据库初始化日志 +docker-compose logs vhd-select-server | grep -i postgres + +# 重新初始化数据库 +docker-compose down -v +docker-compose up -d +``` + +**外部数据库连接问题** +```bash +# 测试数据库连接 +docker run --rm -it postgres:15-alpine psql -h your-db-host -U your-db-user -d vhd_select + +# 检查网络连接 +docker-compose exec vhd-select-server ping your-db-host +``` + +**数据持久化问题** +```bash +# 检查数据卷 +docker volume ls | grep vhd + +# 备份数据 +docker run --rm -v vhd_db_data:/data -v $(pwd):/backup alpine tar czf /backup/vhd_backup.tar.gz /data +``` ### 命令行测试 @@ -246,6 +481,16 @@ python server.py 8081 ## 📝 更新日志 +### v1.2.0 +- ✅ 完整Docker化支持(内置/外部数据库) +- ✅ PostgreSQL数据库集成 +- ✅ 机台管理功能 +- ✅ 机台保护状态控制 +- ✅ 用户认证和会话管理 +- ✅ 完整的RESTful API文档 +- ✅ 健康检查和监控 +- ✅ 数据持久化和备份 + ### v1.0.0 - ✅ 跨平台支持(Node.js + Python) - ✅ 现代化Web界面 diff --git a/VHDSelectServer/database.js b/VHDSelectServer/database.js new file mode 100644 index 0000000..594f67c --- /dev/null +++ b/VHDSelectServer/database.js @@ -0,0 +1,195 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +// 数据库配置 +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT) || 5432, + database: process.env.DB_NAME || 'vhd_select', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'password', + max: parseInt(process.env.DB_MAX_CONNECTIONS) || 20, // 最大连接数 + idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // 空闲连接超时时间 + connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 5000, // 连接超时时间 + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, +}; + +// 创建PostgreSQL连接池 +const pool = new Pool(dbConfig); + +// 数据库操作类 +class Database { + constructor() { + this.pool = pool; + this.initializeDatabase(); + } + + // 初始化数据库连接 + async initializeDatabase() { + const useEmbeddedDb = process.env.USE_EMBEDDED_DB === 'true'; + console.log(`数据库配置: ${useEmbeddedDb ? '内置数据库' : '外部数据库'}`); + console.log(`连接地址: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`); + + try { + const client = await this.pool.connect(); + console.log('PostgreSQL数据库连接成功'); + + // 测试查询 + const result = await client.query('SELECT NOW()'); + console.log('数据库时间:', result.rows[0].now); + + // 检查表是否存在 + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'machines' + ); + `); + + if (tableCheck.rows[0].exists) { + console.log('数据库表结构已存在'); + } else { + console.log('警告: machines表不存在,请确保数据库已正确初始化'); + } + + client.release(); + this.isConnected = true; + } catch (error) { + console.error('数据库连接失败:', error.message); + console.error('错误详情:', error); + + if (useEmbeddedDb) { + console.error('内置数据库连接失败,请检查Docker配置'); + } else { + console.error('外部数据库连接失败,请检查数据库服务器状态和连接配置'); + } + + this.isConnected = false; + throw error; + } + } + + // 获取机台信息 + async getMachine(machineId) { + try { + const client = await this.pool.connect(); + const result = await client.query( + 'SELECT * FROM machines WHERE machine_id = $1', + [machineId] + ); + client.release(); + + return result.rows[0] || null; + } catch (error) { + console.error('获取机台信息失败:', error.message); + return null; + } + } + + // 创建或更新机台信息 + async upsertMachine(machineId, isProtected = false, vhdKeyword = 'SDEZ') { + try { + const client = await this.pool.connect(); + const result = await client.query(` + INSERT INTO machines (machine_id, protected, vhd_keyword) + VALUES ($1, $2, $3) + ON CONFLICT (machine_id) + DO UPDATE SET + protected = EXCLUDED.protected, + vhd_keyword = EXCLUDED.vhd_keyword, + updated_at = CURRENT_TIMESTAMP + RETURNING * + `, [machineId, isProtected, vhdKeyword]); + client.release(); + + return result.rows[0]; + } catch (error) { + console.error('更新机台信息失败:', error.message); + return null; + } + } + + // 更新机台保护状态 + async updateMachineProtection(machineId, isProtected) { + try { + const client = await this.pool.connect(); + const result = await client.query(` + UPDATE machines + SET protected = $2, updated_at = CURRENT_TIMESTAMP + WHERE machine_id = $1 + RETURNING * + `, [machineId, isProtected]); + client.release(); + + return result.rows[0] || null; + } catch (error) { + console.error('更新机台保护状态失败:', error.message); + return null; + } + } + + // 更新机台VHD关键词 + async updateMachineVhdKeyword(machineId, vhdKeyword) { + try { + const client = await this.pool.connect(); + const result = await client.query(` + UPDATE machines + SET vhd_keyword = $2, updated_at = CURRENT_TIMESTAMP + WHERE machine_id = $1 + RETURNING * + `, [machineId, vhdKeyword]); + client.release(); + + return result.rows[0] || null; + } catch (error) { + console.error('更新机台VHD关键词失败:', error.message); + return null; + } + } + + // 获取所有机台信息 + async getAllMachines() { + try { + const client = await this.pool.connect(); + const result = await client.query( + 'SELECT * FROM machines ORDER BY machine_id' + ); + client.release(); + + return result.rows; + } catch (error) { + console.error('获取所有机台信息失败:', error.message); + return []; + } + } + + // 删除机台 + async deleteMachine(machineId) { + try { + const client = await this.pool.connect(); + const result = await client.query( + 'DELETE FROM machines WHERE machine_id = $1 RETURNING *', + [machineId] + ); + client.release(); + + return result.rows[0] || null; + } catch (error) { + console.error('删除机台失败:', error.message); + return null; + } + } + + // 关闭数据库连接池 + async close() { + try { + await this.pool.end(); + console.log('数据库连接池已关闭'); + } catch (error) { + console.error('关闭数据库连接池失败:', error.message); + } + } +} + +module.exports = new Database(); \ No newline at end of file diff --git a/VHDSelectServer/deploy.bat b/VHDSelectServer/deploy.bat deleted file mode 100644 index 3019020..0000000 --- a/VHDSelectServer/deploy.bat +++ /dev/null @@ -1,49 +0,0 @@ -@echo off -echo VHD Select Server Docker 部署脚本 -echo ================================ - -echo 检查Docker是否运行... -docker --version >nul 2>&1 -if %errorlevel% neq 0 ( - echo 错误: Docker未安装或未运行 - echo 请先安装并启动Docker Desktop - pause - exit /b 1 -) - -echo 检查Docker Compose... -docker-compose --version >nul 2>&1 -if %errorlevel% neq 0 ( - echo 错误: Docker Compose未安装 - pause - exit /b 1 -) - -echo 创建配置目录... -if not exist "config" mkdir config - -echo 拉取并启动服务... -docker-compose up -d - -if %errorlevel% equ 0 ( - echo. - echo ✅ 部署成功! - echo 🌐 Web界面: http://localhost:8080 - echo 📁 配置文件: ./config/vhd-config.json - echo. - echo 常用命令: - echo 查看日志: docker-compose logs -f - echo 停止服务: docker-compose down - echo 重启服务: docker-compose restart -) else ( - echo. - echo ❌ 部署失败,请检查错误信息 - echo 如果是网络问题,请尝试: - echo 1. 检查网络连接 - echo 2. 配置Docker镜像源 - echo 3. 使用本地Node.js部署: npm start -) - -echo. -echo 按任意键退出... -pause >nul \ No newline at end of file diff --git a/VHDSelectServer/deploy.sh b/VHDSelectServer/deploy.sh deleted file mode 100644 index 56e6852..0000000 --- a/VHDSelectServer/deploy.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -echo "VHD Select Server Docker 部署脚本" -echo "================================" - -# 检查Docker是否安装 -if ! command -v docker &> /dev/null; then - echo "❌ 错误: Docker未安装" - echo "请先安装Docker: https://docs.docker.com/get-docker/" - exit 1 -fi - -# 检查Docker是否运行 -if ! docker info &> /dev/null; then - echo "❌ 错误: Docker未运行" - echo "请启动Docker服务" - exit 1 -fi - -echo "✅ Docker检查通过" - -# 检查Docker Compose -if ! command -v docker-compose &> /dev/null; then - echo "❌ 错误: Docker Compose未安装" - echo "请安装Docker Compose" - exit 1 -fi - -echo "✅ Docker Compose检查通过" - -# 创建配置目录 -echo "📁 创建配置目录..." -mkdir -p config - -# 拉取并启动服务 -echo "🚀 拉取并启动服务..." -if docker-compose up -d; then - echo "" - echo "✅ 部署成功!" - echo "🌐 Web界面: http://localhost:8080" - echo "📁 配置文件: ./config/vhd-config.json" - echo "" - echo "常用命令:" - echo " 查看日志: docker-compose logs -f" - echo " 停止服务: docker-compose down" - echo " 重启服务: docker-compose restart" - echo " 查看状态: docker-compose ps" -else - echo "" - echo "❌ 部署失败,请检查错误信息" - echo "如果是网络问题,请尝试:" - echo " 1. 检查网络连接" - echo " 2. 配置Docker镜像源" - echo " 3. 使用本地Node.js部署: npm start" - exit 1 -fi - -echo "" -echo "🎉 部署完成!" \ No newline at end of file diff --git a/VHDSelectServer/docker-compose.external-db.yml b/VHDSelectServer/docker-compose.external-db.yml new file mode 100644 index 0000000..8fc545d --- /dev/null +++ b/VHDSelectServer/docker-compose.external-db.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + vhd-select-server: + image: lty271104/vhd-select-server:latest + ports: + - "8080:8080" + volumes: + - ./config:/app/config + - ./vhd-data:/app/vhd-data + environment: + # 应用配置 + - NODE_ENV=production + - PORT=8080 + - CONFIG_PATH=/app/config + + # 数据库配置 - 使用外部数据库 + - USE_EMBEDDED_DB=false + - DB_HOST=postgres + - DB_PORT=5432 + - DB_NAME=vhd_select + - DB_USER=postgres + - DB_PASSWORD=vhd_select_password + + # 可选的数据库连接配置 + - DB_MAX_CONNECTIONS=20 + - DB_IDLE_TIMEOUT=30000 + - DB_CONNECTION_TIMEOUT=5000 + - DB_SSL=false + depends_on: + - postgres + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: unless-stopped + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=vhd_select + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=vhd_select_password + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql + ports: + - "5432:5432" + restart: unless-stopped + +volumes: + postgres_data: + driver: local \ No newline at end of file diff --git a/VHDSelectServer/docker-compose.yml b/VHDSelectServer/docker-compose.yml index a8521c8..2a22402 100644 --- a/VHDSelectServer/docker-compose.yml +++ b/VHDSelectServer/docker-compose.yml @@ -3,26 +3,57 @@ version: '3.8' services: vhd-select-server: image: lty271104/vhd-select-server:latest - container_name: vhd-select-server ports: - - "8080:8080" + - "8082:8080" volumes: - # 配置文件持久化映射 - ./config:/app/config - # 可选:映射整个数据目录 - - vhd-data:/app/data + - ./vhd-data:/app/vhd-data + # 内置数据库数据持久化 + - vhd_db_data:/var/lib/postgresql/data environment: + # 应用配置 - NODE_ENV=production - PORT=8080 - CONFIG_PATH=/app/config - restart: unless-stopped + + # 数据库配置 - 使用内置数据库 + - USE_EMBEDDED_DB=true + - DB_HOST=localhost + - DB_PORT=5432 + - DB_NAME=vhd_select + - DB_USER=postgres + - DB_PASSWORD=vhd_select_password + + # 可选的数据库连接配置 + - DB_MAX_CONNECTIONS=20 + - DB_IDLE_TIMEOUT=30000 + - DB_CONNECTION_TIMEOUT=5000 + - DB_SSL=false healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/status"] + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 30s timeout: 10s retries: 3 - start_period: 40s + start_period: 60s + restart: unless-stopped + + # 外部数据库示例配置(可选) + # 如果要使用外部数据库,请取消注释以下配置并修改上面的环境变量 + # postgres: + # image: postgres:15-alpine + # environment: + # - POSTGRES_DB=vhd_select + # - POSTGRES_USER=postgres + # - POSTGRES_PASSWORD=vhd_select_password + # volumes: + # - postgres_data:/var/lib/postgresql/data + # - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql + # ports: + # - "5432:5432" + # restart: unless-stopped volumes: - vhd-data: - driver: local \ No newline at end of file + vhd_db_data: + driver: local + # postgres_data: + # driver: local \ No newline at end of file diff --git a/VHDSelectServer/docker-entrypoint.sh b/VHDSelectServer/docker-entrypoint.sh new file mode 100644 index 0000000..8c43562 --- /dev/null +++ b/VHDSelectServer/docker-entrypoint.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e + +# 日志函数 +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +# 等待外部数据库连接 +wait_for_external_db() { + log "等待外部数据库连接..." + local max_attempts=30 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" 2>/dev/null; then + log "外部数据库连接成功" + return 0 + fi + + log "尝试连接外部数据库 ($attempt/$max_attempts)..." + sleep 2 + attempt=$((attempt + 1)) + done + + log "错误: 无法连接到外部数据库" + exit 1 +} + +# 初始化内置PostgreSQL数据库 +init_embedded_db() { + log "初始化内置PostgreSQL数据库..." + + # 检查数据目录是否已初始化 + if [ ! -f "$POSTGRES_DATA_DIR/PG_VERSION" ]; then + log "初始化PostgreSQL数据目录..." + su-exec postgres initdb -D "$POSTGRES_DATA_DIR" --auth-local=trust --auth-host=md5 + + # 配置PostgreSQL + echo "host all all 0.0.0.0/0 md5" >> "$POSTGRES_DATA_DIR/pg_hba.conf" + echo "listen_addresses = '*'" >> "$POSTGRES_DATA_DIR/postgresql.conf" + echo "port = $DB_PORT" >> "$POSTGRES_DATA_DIR/postgresql.conf" + echo "max_connections = 100" >> "$POSTGRES_DATA_DIR/postgresql.conf" + echo "shared_buffers = 128MB" >> "$POSTGRES_DATA_DIR/postgresql.conf" + echo "log_statement = 'all'" >> "$POSTGRES_DATA_DIR/postgresql.conf" + echo "logging_collector = on" >> "$POSTGRES_DATA_DIR/postgresql.conf" + fi + + # 启动PostgreSQL + log "启动PostgreSQL服务..." + su-exec postgres pg_ctl -D "$POSTGRES_DATA_DIR" -l "$POSTGRES_DATA_DIR/postgresql.log" start + + # 等待PostgreSQL启动 + local max_attempts=30 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + if su-exec postgres pg_isready -h localhost -p "$DB_PORT" 2>/dev/null; then + log "PostgreSQL启动成功" + break + fi + log "等待PostgreSQL启动 ($attempt/$max_attempts)..." + sleep 1 + attempt=$((attempt + 1)) + done + + if [ $attempt -gt $max_attempts ]; then + log "错误: PostgreSQL启动失败" + log "PostgreSQL日志内容:" + cat "$POSTGRES_DATA_DIR/postgresql.log" || true + exit 1 + fi + + # 创建数据库用户和数据库 + log "创建数据库用户和数据库..." + su-exec postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true + su-exec postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" 2>/dev/null || true + su-exec postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" 2>/dev/null || true + + # 初始化数据库表结构 + log "初始化数据库表结构..." + if [ -f "/app/init-db.sql" ]; then + # 使用postgres用户通过本地socket连接执行初始化脚本 + su-exec postgres psql -d "$DB_NAME" -f /app/init-db.sql + su-exec postgres psql -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;" + su-exec postgres psql -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;" + fi + + log "内置数据库初始化完成" +} + +# 初始化外部数据库表结构 +init_external_db() { + log "初始化外部数据库表结构..." + + # 检查是否需要初始化表结构 + if ! PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1 FROM machines LIMIT 1;" 2>/dev/null; then + log "创建数据库表结构..." + PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f /app/scripts/init-db.sql + log "外部数据库表结构初始化完成" + else + log "外部数据库表结构已存在,跳过初始化" + fi +} + +# 启动Node.js应用 +start_app() { + log "启动VHD Select Server应用..." + cd /app + su-exec nodejs npm start +} + +# 优雅关闭处理 +cleanup() { + log "接收到关闭信号,正在优雅关闭..." + + # 关闭Node.js应用 + if [ ! -z "$APP_PID" ]; then + kill -TERM "$APP_PID" 2>/dev/null || true + wait "$APP_PID" 2>/dev/null || true + fi + + # 如果使用内置数据库,关闭PostgreSQL + if [ "$USE_EMBEDDED_DB" = "true" ]; then + log "关闭内置PostgreSQL数据库..." + su-exec postgres pg_ctl -D "$POSTGRES_DATA_DIR" stop -m fast 2>/dev/null || true + fi + + log "应用已关闭" + exit 0 +} + +# 设置信号处理 +trap cleanup SIGTERM SIGINT + +# 主逻辑 +log "VHD Select Server Docker容器启动" +log "USE_EMBEDDED_DB: $USE_EMBEDDED_DB" + +if [ "$USE_EMBEDDED_DB" = "true" ]; then + log "使用内置PostgreSQL数据库" + init_embedded_db +else + log "使用外部数据库: $DB_HOST:$DB_PORT/$DB_NAME" + wait_for_external_db + init_external_db +fi + +# 启动应用(在后台运行以便处理信号) +start_app & +APP_PID=$! + +# 等待应用进程 +wait "$APP_PID" \ No newline at end of file diff --git a/VHDSelectServer/init-db.sql b/VHDSelectServer/init-db.sql new file mode 100644 index 0000000..a06797e --- /dev/null +++ b/VHDSelectServer/init-db.sql @@ -0,0 +1,48 @@ +-- VHD Select Server 数据库初始化脚本 + +-- 创建数据库(如果不存在) +-- CREATE DATABASE vhd_select; + +-- 使用数据库 +-- \c vhd_select; + +-- 创建机台表 +CREATE TABLE IF NOT EXISTS machines ( + id SERIAL PRIMARY KEY, + machine_id VARCHAR(255) UNIQUE NOT NULL, + protected BOOLEAN DEFAULT FALSE, + vhd_keyword VARCHAR(255) DEFAULT 'SDEZ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建更新时间触发器函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为machines表创建更新时间触发器 +DROP TRIGGER IF EXISTS update_machines_updated_at ON machines; +CREATE TRIGGER update_machines_updated_at + BEFORE UPDATE ON machines + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 插入一些示例数据 +INSERT INTO machines (machine_id, protected, vhd_keyword) +VALUES + ('MACHINE001', FALSE, 'SDEZ'), + ('MACHINE002', FALSE, 'PROD'), + ('MACHINE003', TRUE, 'TEST') +ON CONFLICT (machine_id) DO NOTHING; + +-- 创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_machines_machine_id ON machines(machine_id); +CREATE INDEX IF NOT EXISTS idx_machines_protected ON machines(protected); + +-- 显示表结构 +\d machines; \ No newline at end of file diff --git a/VHDSelectServer/init-embedded-db.sh b/VHDSelectServer/init-embedded-db.sh new file mode 100644 index 0000000..0d35f2b --- /dev/null +++ b/VHDSelectServer/init-embedded-db.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# 内置PostgreSQL数据库初始化脚本 +# 此脚本在Docker容器启动时运行,用于初始化内置PostgreSQL数据库 + +set -e + +# 日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +# 数据库配置 +DB_NAME="${DB_NAME:-vhd_select}" +DB_USER="${DB_USER:-postgres}" +DB_PASSWORD="${DB_PASSWORD:-vhd_select_password}" +PGDATA="${PGDATA:-/var/lib/postgresql/data}" + +log "开始初始化内置PostgreSQL数据库..." + +# 检查数据目录是否已初始化 +if [ ! -s "$PGDATA/PG_VERSION" ]; then + log "初始化PostgreSQL数据目录..." + + # 初始化数据库 + su-exec postgres initdb -D "$PGDATA" --auth-local=trust --auth-host=md5 + + # 启动PostgreSQL服务 + log "启动PostgreSQL服务..." + su-exec postgres pg_ctl -D "$PGDATA" -l "$PGDATA/postgresql.log" start + + # 等待PostgreSQL启动 + log "等待PostgreSQL服务启动..." + for i in {1..30}; do + if su-exec postgres pg_isready -q; then + log "PostgreSQL服务已启动" + break + fi + if [ $i -eq 30 ]; then + log "错误: PostgreSQL服务启动超时" + exit 1 + fi + sleep 1 + done + + # 创建数据库和用户 + log "创建数据库和用户..." + su-exec postgres psql -v ON_ERROR_STOP=1 <<-EOSQL + CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD'; + CREATE DATABASE $DB_NAME OWNER $DB_USER; + GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER; +EOSQL + + # 执行初始化SQL脚本 + if [ -f "/app/init-db.sql" ]; then + log "执行数据库初始化脚本..." + su-exec postgres psql -v ON_ERROR_STOP=1 -d "$DB_NAME" -f "/app/init-db.sql" + log "数据库初始化脚本执行完成" + else + log "警告: 未找到数据库初始化脚本 /app/init-db.sql" + fi + + # 停止PostgreSQL服务 + log "停止PostgreSQL服务..." + su-exec postgres pg_ctl -D "$PGDATA" stop + + log "内置PostgreSQL数据库初始化完成" +else + log "PostgreSQL数据目录已存在,跳过初始化" +fi + +log "内置数据库初始化脚本执行完成" \ No newline at end of file diff --git a/VHDSelectServer/package.json b/VHDSelectServer/package.json index bece156..98c041a 100644 --- a/VHDSelectServer/package.json +++ b/VHDSelectServer/package.json @@ -1,6 +1,6 @@ { "name": "vhd-select-server", - "version": "1.0.0", + "version": "1.2.0", "description": "Cross-platform VHD selection server with web GUI", "main": "server.js", "scripts": { @@ -19,7 +19,11 @@ "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", - "body-parser": "^1.20.2" + "body-parser": "^1.20.2", + "pg": "^8.11.3", + "dotenv": "^16.3.1", + "express-session": "^1.17.3", + "bcryptjs": "^2.4.3" }, "engines": { "node": ">=14.0.0" diff --git a/VHDSelectServer/server.js b/VHDSelectServer/server.js index 5228f02..13d995b 100644 --- a/VHDSelectServer/server.js +++ b/VHDSelectServer/server.js @@ -3,15 +3,37 @@ const cors = require('cors'); const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs'); +const session = require('express-session'); +const bcrypt = require('bcryptjs'); +require('dotenv').config(); + +const database = require('./database'); const app = express(); const PORT = process.env.PORT || 8080; +// 管理员密码配置 (默认密码: admin123) +const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || bcrypt.hashSync('admin123', 10); + +// 会话配置 +app.use(session({ + secret: process.env.SESSION_SECRET || 'vhd-select-server-secret-key-2024', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // 在生产环境中应该设置为true (需要HTTPS) + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24小时 + } +})); + // 中间件 -app.use(cors()); +app.use(cors({ + origin: true, + credentials: true // 允许发送cookies +})); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); -app.use(express.static(path.join(__dirname, 'public'))); // VHD关键词存储 let currentVhdKeyword = 'SDEZ'; // 默认值 @@ -47,20 +69,129 @@ function saveConfig() { } } -// API路由 +// 认证中间件 +function requireAuth(req, res, next) { + if (req.session && req.session.isAuthenticated) { + return next(); + } else { + return res.status(401).json({ + success: false, + message: '需要登录', + requireAuth: true + }); + } +} -// 获取当前VHD关键词 -app.get('/api/boot-image-select', (req, res) => { - console.log(`[${new Date().toISOString()}] API请求: GET /api/boot-image-select`); - res.json({ - success: true, - BootImageSelected: currentVhdKeyword, - timestamp: new Date().toISOString() +// 静态文件服务 (登录页面不需要认证) +app.use(express.static(path.join(__dirname, 'public'))); + +// 认证API路由 +app.post('/api/auth/login', async (req, res) => { + try { + const { password } = req.body; + + console.log(`[${new Date().toISOString()}] 登录尝试`); + + if (!password) { + return res.status(400).json({ + success: false, + message: '请输入密码' + }); + } + + // 验证密码 + const isValidPassword = await bcrypt.compare(password, ADMIN_PASSWORD_HASH); + + if (isValidPassword) { + req.session.isAuthenticated = true; + console.log(`[${new Date().toISOString()}] 登录成功`); + res.json({ + success: true, + message: '登录成功' + }); + } else { + console.log(`[${new Date().toISOString()}] 登录失败: 密码错误`); + res.status(401).json({ + success: false, + message: '密码错误' + }); + } + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +app.post('/api/auth/logout', (req, res) => { + req.session.destroy((err) => { + if (err) { + console.error('登出错误:', err); + return res.status(500).json({ + success: false, + message: '登出失败' + }); + } + res.json({ + success: true, + message: '已登出' + }); }); }); -// 设置VHD关键词 -app.post('/api/set-vhd', (req, res) => { +app.get('/api/auth/check', (req, res) => { + res.json({ + isAuthenticated: !!(req.session && req.session.isAuthenticated) + }); +}); + +// API路由 + +// 获取当前VHD关键词 - 需要Machine ID验证 +app.get('/api/boot-image-select', async (req, res) => { + const machineId = req.query.machineId; + + console.log(`[${new Date().toISOString()}] API请求: GET /api/boot-image-select, Machine ID: ${machineId}`); + + if (!machineId) { + return res.status(400).json({ + success: false, + error: 'Machine ID是必需的' + }); + } + + try { + // 从数据库获取机台信息 + let machine = await database.getMachine(machineId); + + // 如果机台不存在,创建新的机台记录 + if (!machine) { + machine = await database.upsertMachine(machineId, false, currentVhdKeyword); + console.log(`[${new Date().toISOString()}] 创建新机台: ${machineId}`); + } + + res.json({ + success: true, + BootImageSelected: machine ? machine.vhd_keyword : currentVhdKeyword, + machineId: machineId, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('获取VHD关键词失败:', error.message); + // 降级到内存存储 + res.json({ + success: true, + BootImageSelected: currentVhdKeyword, + machineId: machineId, + timestamp: new Date().toISOString() + }); + } +}); + +// 设置VHD关键词 (需要认证) +app.post('/api/set-vhd', requireAuth, (req, res) => { const { BootImageSelected } = req.body; if (!BootImageSelected || typeof BootImageSelected !== 'string') { @@ -91,6 +222,194 @@ app.post('/api/set-vhd', (req, res) => { }); }); +// 保护状态检查端点 - 需要Machine ID验证 +app.get('/api/protect', async (req, res) => { + const machineId = req.query.machineId; + + console.log(`[${new Date().toISOString()}] API请求: GET /api/protect, Machine ID: ${machineId}`); + + if (!machineId) { + return res.status(400).json({ + success: false, + error: 'Machine ID是必需的' + }); + } + + try { + // 从数据库获取机台信息 + let machine = await database.getMachine(machineId); + + // 如果机台不存在,创建新的机台记录 + if (!machine) { + machine = await database.upsertMachine(machineId, false, currentVhdKeyword); + console.log(`[${new Date().toISOString()}] 创建新机台: ${machineId}`); + } + + res.json({ + success: true, + protected: machine ? machine.protected : false, + machineId: machineId, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('获取保护状态失败:', error.message); + // 降级到默认状态 + res.json({ + success: true, + protected: false, + machineId: machineId, + timestamp: new Date().toISOString() + }); + } +}); + +// 设置机台保护状态 +app.post('/api/protect', requireAuth, async (req, res) => { + const { machineId, protected } = req.body; + + console.log(`[${new Date().toISOString()}] API请求: POST /api/protect, Machine ID: ${machineId}, Protected: ${protected}`); + + if (!machineId) { + return res.status(400).json({ + success: false, + error: 'Machine ID是必需的' + }); + } + + if (typeof protected !== 'boolean') { + return res.status(400).json({ + success: false, + error: 'protected状态必须是布尔值' + }); + } + + try { + // 更新机台保护状态 + const machine = await database.updateMachineProtection(machineId, protected); + + if (!machine) { + // 如果机台不存在,创建新的机台记录 + const newMachine = await database.upsertMachine(machineId, protected, currentVhdKeyword); + return res.json({ + success: true, + protected: newMachine.protected, + machineId: machineId, + message: '机台保护状态已设置' + }); + } + + res.json({ + success: true, + protected: machine.protected, + machineId: machineId, + message: '机台保护状态已更新' + }); + } catch (error) { + console.error('设置保护状态失败:', error.message); + res.status(500).json({ + success: false, + error: '设置保护状态失败' + }); + } +}); + +// 获取所有机台信息 +app.get('/api/machines', requireAuth, async (req, res) => { + console.log(`[${new Date().toISOString()}] API请求: GET /api/machines`); + + try { + const machines = await database.getAllMachines(); + res.json({ + success: true, + machines: machines, + count: machines.length, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('获取机台列表失败:', error.message); + res.status(500).json({ + success: false, + error: '获取机台列表失败' + }); + } +}); + +// 设置特定机台的VHD关键词 +app.post('/api/machines/:machineId/vhd', requireAuth, async (req, res) => { + const { machineId } = req.params; + const { vhdKeyword } = req.body; + + console.log(`[${new Date().toISOString()}] API请求: POST /api/machines/${machineId}/vhd, VHD: ${vhdKeyword}`); + + if (!vhdKeyword || typeof vhdKeyword !== 'string') { + return res.status(400).json({ + success: false, + error: 'VHD关键词不能为空且必须是字符串' + }); + } + + const trimmedKeyword = vhdKeyword.trim().toUpperCase(); + if (trimmedKeyword.length === 0) { + return res.status(400).json({ + success: false, + error: 'VHD关键词不能为空' + }); + } + + try { + // 更新机台VHD关键词 + let machine = await database.updateMachineVhdKeyword(machineId, trimmedKeyword); + + if (!machine) { + // 如果机台不存在,创建新的机台记录 + machine = await database.upsertMachine(machineId, false, trimmedKeyword); + } + + res.json({ + success: true, + machineId: machineId, + vhdKeyword: machine.vhd_keyword, + message: '机台VHD关键词已更新' + }); + } catch (error) { + console.error('设置机台VHD关键词失败:', error.message); + res.status(500).json({ + success: false, + error: '设置机台VHD关键词失败' + }); + } +}); + +// 删除机台 +app.delete('/api/machines/:machineId', requireAuth, async (req, res) => { + const { machineId } = req.params; + + console.log(`[${new Date().toISOString()}] API请求: DELETE /api/machines/${machineId}`); + + try { + const deletedMachine = await database.deleteMachine(machineId); + + if (!deletedMachine) { + return res.status(404).json({ + success: false, + error: '机台不存在' + }); + } + + res.json({ + success: true, + machineId: machineId, + message: '机台已删除' + }); + } catch (error) { + console.error('删除机台失败:', error.message); + res.status(500).json({ + success: false, + error: '删除机台失败' + }); + } +}); + // 获取服务器状态 app.get('/api/status', (req, res) => { res.json({ @@ -99,7 +418,7 @@ app.get('/api/status', (req, res) => { BootImageSelected: currentVhdKeyword, uptime: process.uptime(), timestamp: new Date().toISOString(), - version: '1.0.0' + version: '1.2.0' }); }); @@ -139,14 +458,16 @@ app.listen(PORT, () => { }); // 优雅关闭 -process.on('SIGINT', () => { +process.on('SIGINT', async () => { console.log('\n正在关闭服务器...'); saveConfig(); + await database.close(); process.exit(0); }); -process.on('SIGTERM', () => { +process.on('SIGTERM', async () => { console.log('\n正在关闭服务器...'); saveConfig(); + await database.close(); process.exit(0); }); \ No newline at end of file diff --git a/VHDSelectServer/server.py b/VHDSelectServer/server.py deleted file mode 100644 index 9d33ff0..0000000 --- a/VHDSelectServer/server.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -VHD选择服务器 - Python版本 -跨平台的VHD关键词管理服务器,带有Web GUI界面 -""" - -import json -import os -import sys -from datetime import datetime -from http.server import HTTPServer, BaseHTTPRequestHandler -from urllib.parse import urlparse, parse_qs -import threading -import webbrowser -from pathlib import Path - -class VHDSelectHandler(BaseHTTPRequestHandler): - def __init__(self, *args, **kwargs): - self.config_file = Path(__file__).parent / 'vhd-config.json' - super().__init__(*args, **kwargs) - - def load_config(self): - """加载VHD配置""" - try: - if self.config_file.exists(): - with open(self.config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - return config.get('vhdKeyword', 'SDEZ') - except Exception as e: - print(f"配置加载失败: {e}") - return 'SDEZ' - - def save_config(self, vhd_keyword): - """保存VHD配置""" - try: - config = {'vhdKeyword': vhd_keyword} - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(config, f, ensure_ascii=False, indent=2) - return True - except Exception as e: - print(f"配置保存失败: {e}") - return False - - def send_json_response(self, data, status_code=200): - """发送JSON响应""" - self.send_response(status_code) - self.send_header('Content-Type', 'application/json; charset=utf-8') - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') - self.send_header('Access-Control-Allow-Headers', 'Content-Type') - self.end_headers() - - json_str = json.dumps(data, ensure_ascii=False, indent=2) - self.wfile.write(json_str.encode('utf-8')) - - def send_html_file(self, file_path): - """发送HTML文件""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(content.encode('utf-8')) - except FileNotFoundError: - self.send_error(404, '文件未找到') - except Exception as e: - self.send_error(500, f'服务器错误: {e}') - - def do_OPTIONS(self): - """处理CORS预检请求""" - self.send_response(200) - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') - self.send_header('Access-Control-Allow-Headers', 'Content-Type') - self.end_headers() - - def do_GET(self): - """处理GET请求""" - parsed_path = urlparse(self.path) - path = parsed_path.path - - print(f"[{datetime.now().isoformat()}] GET {path}") - - if path == '/': - # 主页 - html_file = Path(__file__).parent / 'public' / 'index.html' - self.send_html_file(html_file) - - elif path == '/api/boot-image-select': - # 获取当前VHD关键词 - current_keyword = self.load_config() - response = { - 'success': True, - 'BootImageSelected': current_keyword, - 'timestamp': datetime.now().isoformat() - } - self.send_json_response(response) - - elif path == '/api/status': - # 服务器状态 - current_keyword = self.load_config() - response = { - 'success': True, - 'status': 'running', - 'BootImageSelected': current_keyword, - 'timestamp': datetime.now().isoformat(), - 'version': '1.0.0 (Python)' - } - self.send_json_response(response) - - else: - # 404错误 - response = { - 'success': False, - 'error': '页面未找到' - } - self.send_json_response(response, 404) - - def do_POST(self): - """处理POST请求""" - parsed_path = urlparse(self.path) - path = parsed_path.path - - print(f"[{datetime.now().isoformat()}] POST {path}") - - if path == '/api/set-vhd': - try: - # 读取请求体 - content_length = int(self.headers.get('Content-Length', 0)) - post_data = self.rfile.read(content_length) - - # 解析JSON数据 - data = json.loads(post_data.decode('utf-8')) - vhd_keyword = data.get('BootImageSelected', '').strip().upper() - - if not vhd_keyword: - response = { - 'success': False, - 'error': 'VHD关键词不能为空' - } - self.send_json_response(response, 400) - return - - # 保存配置 - if self.save_config(vhd_keyword): - response = { - 'success': True, - 'BootImageSelected': vhd_keyword, - 'message': 'VHD关键词更新成功' - } - print(f"VHD关键词已更新为: {vhd_keyword}") - self.send_json_response(response) - else: - response = { - 'success': False, - 'error': '配置保存失败' - } - self.send_json_response(response, 500) - - except json.JSONDecodeError: - response = { - 'success': False, - 'error': '无效的JSON数据' - } - self.send_json_response(response, 400) - except Exception as e: - response = { - 'success': False, - 'error': f'服务器错误: {str(e)}' - } - self.send_json_response(response, 500) - else: - # 404错误 - response = { - 'success': False, - 'error': '页面未找到' - } - self.send_json_response(response, 404) - - def log_message(self, format, *args): - """自定义日志格式""" - pass # 禁用默认日志,使用自定义日志 - -def start_server(port=8080): - """启动服务器""" - server_address = ('', port) - httpd = HTTPServer(server_address, VHDSelectHandler) - - print('=' * 50) - print('🚀 VHD选择服务器已启动 (Python版本)') - print(f'📡 服务器地址: http://localhost:{port}') - print(f'🔧 API地址: http://localhost:{port}/api/boot-image-select') - print(f'📊 状态页面: http://localhost:{port}/api/status') - - # 加载当前配置 - config_file = Path(__file__).parent / 'vhd-config.json' - try: - if config_file.exists(): - with open(config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - current_keyword = config.get('vhdKeyword', 'SDEZ') - else: - current_keyword = 'SDEZ' - except: - current_keyword = 'SDEZ' - - print(f'🎯 当前VHD关键词: {current_keyword}') - print('=' * 50) - print('💡 提示: 按 Ctrl+C 停止服务器') - print() - - # 自动打开浏览器 - def open_browser(): - import time - time.sleep(1) # 等待服务器启动 - try: - webbrowser.open(f'http://localhost:{port}') - except: - pass - - browser_thread = threading.Thread(target=open_browser) - browser_thread.daemon = True - browser_thread.start() - - try: - httpd.serve_forever() - except KeyboardInterrupt: - print('\n正在关闭服务器...') - httpd.shutdown() - print('服务器已关闭') - -if __name__ == '__main__': - # 检查命令行参数 - port = 8080 - if len(sys.argv) > 1: - try: - port = int(sys.argv[1]) - except ValueError: - print('错误: 端口号必须是数字') - sys.exit(1) - - start_server(port) \ No newline at end of file diff --git a/VHDSelectServer/setup_database.bat b/VHDSelectServer/setup_database.bat new file mode 100644 index 0000000..26385d6 --- /dev/null +++ b/VHDSelectServer/setup_database.bat @@ -0,0 +1,68 @@ +@echo off +chcp 65001 >nul +echo. +echo ======================================== +echo PostgreSQL Database Setup Script +echo ======================================== +echo. + +REM Check if PostgreSQL is installed +echo Checking PostgreSQL installation... +psql --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ERROR: PostgreSQL is not installed or not in PATH + echo. + echo Please install PostgreSQL first: + echo 1. Download from: https://www.postgresql.org/download/windows/ + echo 2. Run the installer as administrator + echo 3. Set superuser password to: password + echo 4. Use default port: 5432 + echo 5. Add PostgreSQL bin directory to PATH + echo. + echo After installation, run this script again. + pause + exit /b 1 +) + +echo SUCCESS: PostgreSQL is installed +echo. + +REM Prompt for PostgreSQL superuser password +echo Please enter PostgreSQL superuser password: +set /p "PGPASSWORD=Password: " +echo. + +REM Set environment variable for password +set PGPASSWORD=%PGPASSWORD% + +echo Setting up database... +echo. + +REM Execute the SQL setup script +psql -U postgres -h localhost -f setup_postgresql.sql + +if %errorlevel% equ 0 ( + echo. + echo SUCCESS: Database setup completed successfully! + echo. + echo Database Information: + echo Host: localhost + echo Port: 5432 + echo Database: vhd_select + echo User: postgres + echo. + echo You can now start the VHDSelectServer: + echo node server.js + echo. +) else ( + echo. + echo ERROR: Database setup failed! + echo Please check the error messages above and try again. + echo. +) + +REM Clear password from environment +set PGPASSWORD= + +echo Press any key to exit... +pause >nul \ No newline at end of file diff --git a/VHDSelectServer/setup_postgresql.sql b/VHDSelectServer/setup_postgresql.sql new file mode 100644 index 0000000..0ef6771 --- /dev/null +++ b/VHDSelectServer/setup_postgresql.sql @@ -0,0 +1,55 @@ +-- PostgreSQL数据库配置脚本 +-- 请在安装PostgreSQL后,使用管理员权限运行此脚本 + +-- 1. 创建数据库 +CREATE DATABASE vhd_select + WITH + OWNER = postgres + ENCODING = 'UTF8' + LC_COLLATE = 'Chinese (Simplified)_China.936' + LC_CTYPE = 'Chinese (Simplified)_China.936' + TABLESPACE = pg_default + CONNECTION LIMIT = -1; + +-- 2. 连接到新创建的数据库 +\c vhd_select; + +-- 3. 创建machines表 +CREATE TABLE IF NOT EXISTS machines ( + id SERIAL PRIMARY KEY, + machine_id VARCHAR(255) UNIQUE NOT NULL, + protected BOOLEAN DEFAULT FALSE, + vhd_keyword VARCHAR(50) DEFAULT 'SDEZ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 4. 创建更新时间戳的触发器函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 5. 创建触发器 +CREATE TRIGGER update_machines_updated_at + BEFORE UPDATE ON machines + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 6. 创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_machines_machine_id ON machines(machine_id); +CREATE INDEX IF NOT EXISTS idx_machines_protected ON machines(protected); + +-- 7. 插入示例数据 +INSERT INTO machines (machine_id, protected, vhd_keyword) VALUES +('MACHINE001', false, 'SDEZ'), +('MACHINE002', true, 'SDHC'), +('MACHINE003', false, 'SDXC') +ON CONFLICT (machine_id) DO NOTHING; + +-- 8. 显示创建结果 +SELECT 'Database setup completed successfully!' as status; +SELECT * FROM machines; \ No newline at end of file diff --git a/vhdmonter_config.ini b/vhdmonter_config.ini index a77e929..db9c31d 100644 --- a/vhdmonter_config.ini +++ b/vhdmonter_config.ini @@ -3,4 +3,16 @@ EnableRemoteSelection=false ; 远程VHD选择服务的URL -BootImageSelectUrl=http://localhost:8080/api/boot-image-select \ No newline at end of file +BootImageSelectUrl=http://localhost:8080/api/boot-image-select + +; 机台ID (用于识别当前机台) +MachineId=MACHINE_001 + +; 是否启用保护检查功能 (true/false) +EnableProtectionCheck=true + +; 保护检查服务的URL +ProtectionCheckUrl=http://localhost:8080/api/protect + +; 保护检查间隔 (毫秒) +ProtectionCheckInterval=500 \ No newline at end of file