C端打包修复不能在中文路径下启动的问题
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { resolveRuntimeStrategy } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* 配置文件生成器
|
||||
@@ -8,11 +9,12 @@ const path = require('path');
|
||||
class ConfigGenerator {
|
||||
constructor() {
|
||||
// 开发环境:项目根目录
|
||||
// 打包后:应用根目录(win-unpacked)
|
||||
// 打包后:应用根目录(最终交付目录)
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
const pathStrategy = resolveRuntimeStrategy(baseDir);
|
||||
|
||||
// 开发环境:build/extraResources/java
|
||||
// 打包后:resources/extraResources/java
|
||||
@@ -21,9 +23,12 @@ class ConfigGenerator {
|
||||
: path.join(process.resourcesPath, 'extraResources', 'java');
|
||||
this.templatePath = path.join(this.javaPath, 'application.yml.template');
|
||||
this.configPath = path.join(this.javaPath, 'application.yml');
|
||||
this.pathStrategy = pathStrategy;
|
||||
|
||||
// 数据目录(使用应用所在盘符的根目录下的data文件夹)
|
||||
this.dataPath = this.getDataPath(baseDir);
|
||||
// 数据目录:
|
||||
// - 安全路径:使用应用目录内的 NPQS9100_Data
|
||||
// - 非 ASCII 路径:切到英文安全运行根目录下,避免在盘符根目录创建多个文件夹
|
||||
this.dataPath = this.getDataPath(baseDir, pathStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,8 +36,11 @@ class ConfigGenerator {
|
||||
* @param {string} baseDir 应用基础目录
|
||||
* @returns {string} 数据目录路径
|
||||
*/
|
||||
getDataPath(baseDir) {
|
||||
// 数据目录设置在应用目录内的 NPQS9100_Data 文件夹
|
||||
getDataPath(baseDir, pathStrategy = resolveRuntimeStrategy(baseDir)) {
|
||||
if (pathStrategy.usesSafePaths) {
|
||||
return path.join(pathStrategy.safeRuntimeRoot, 'data');
|
||||
}
|
||||
|
||||
return path.join(baseDir, 'NPQS9100_Data');
|
||||
}
|
||||
|
||||
@@ -85,6 +93,7 @@ class ConfigGenerator {
|
||||
this.createDirectories();
|
||||
|
||||
console.log('[ConfigGenerator] Configuration file generated successfully');
|
||||
console.log('[ConfigGenerator] Path mode:', this.pathStrategy.usesSafePaths ? 'safe-data-root' : 'app-local-data');
|
||||
console.log('[ConfigGenerator] Data path:', this.dataPath);
|
||||
console.log('[ConfigGenerator] MySQL port:', options.mysqlPort || 3306);
|
||||
console.log('[ConfigGenerator] MySQL password:', options.mysqlPassword || 'njcnpqs');
|
||||
|
||||
@@ -9,7 +9,7 @@ class JavaRunner {
|
||||
constructor() {
|
||||
// 在开发与打包后均可解析到应用根目录下的 jre 目录
|
||||
// 开发环境:项目根目录
|
||||
// 打包后:应用根目录(win-unpacked)
|
||||
// 打包后:应用根目录(最终交付目录)
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
const { spawn, exec } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { resolveRuntimeStrategy } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* MySQL 进程管理器
|
||||
* 使用进程模式启动 MySQL,无需管理员权限
|
||||
*/
|
||||
class MySQLServiceManager {
|
||||
class MySQLProcessManager {
|
||||
constructor(logWindowManager = null) {
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
const pathStrategy = resolveRuntimeStrategy(baseDir);
|
||||
|
||||
this.mysqlPath = path.join(baseDir, 'mysql');
|
||||
this.baseDir = baseDir;
|
||||
this.pathStrategy = pathStrategy;
|
||||
this.sourceMysqlPath = path.join(baseDir, 'mysql');
|
||||
this.mysqlPath = pathStrategy.usesSafePaths
|
||||
? path.join(pathStrategy.safeRuntimeRoot, 'mysql')
|
||||
: this.sourceMysqlPath;
|
||||
this.binPath = path.join(this.mysqlPath, 'bin');
|
||||
this.dataPath = path.join(this.mysqlPath, 'data');
|
||||
this.configFile = path.join(this.mysqlPath, 'my.ini');
|
||||
@@ -37,6 +44,106 @@ class MySQLServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录。
|
||||
* 仅当文件不存在、大小变化或源文件时间更新时才覆盖,避免每次启动全量重写。
|
||||
*/
|
||||
copyDirectorySync(sourceDir, targetDir, options = {}) {
|
||||
const {
|
||||
excludeTopLevelDirs = new Set(),
|
||||
excludeFileNames = new Set(),
|
||||
excludeFileExtensions = new Set()
|
||||
} = options;
|
||||
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const targetPath = path.join(targetDir, entry.name);
|
||||
const isTopLevelEntry = sourceDir === this.sourceMysqlPath;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (isTopLevelEntry && excludeTopLevelDirs.has(entry.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copyDirectorySync(sourcePath, targetPath, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = path.extname(entry.name).toLowerCase();
|
||||
if (excludeFileNames.has(entry.name) || excludeFileExtensions.has(extension)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldCopy = !fs.existsSync(targetPath);
|
||||
if (!shouldCopy) {
|
||||
const sourceStat = fs.statSync(sourcePath);
|
||||
const targetStat = fs.statSync(targetPath);
|
||||
shouldCopy = sourceStat.size !== targetStat.size || sourceStat.mtimeMs > targetStat.mtimeMs;
|
||||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 非 ASCII 路径下,将 MySQL 运行环境同步到英文安全目录。
|
||||
*/
|
||||
ensureRuntimeEnvironment() {
|
||||
if (!this.pathStrategy.usesSafePaths) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('system', '检测到应用路径包含非 ASCII 字符,启用 MySQL 英文安全运行目录');
|
||||
this.log('system', `MySQL 源目录: ${this.sourceMysqlPath}`);
|
||||
this.log('system', `MySQL 运行目录: ${this.mysqlPath}`);
|
||||
this.log('system', `MySQL 数据目录: ${this.dataPath}`);
|
||||
|
||||
fs.mkdirSync(this.mysqlPath, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(this.dataPath), { recursive: true });
|
||||
|
||||
this.copyDirectorySync(this.sourceMysqlPath, this.mysqlPath, {
|
||||
excludeTopLevelDirs: new Set(['data', 'data_backup']),
|
||||
excludeFileNames: new Set(['my.ini', 'mysql_error.log', 'mysql_slow.log']),
|
||||
excludeFileExtensions: new Set(['.pid', '.err'])
|
||||
});
|
||||
|
||||
this.ensureSeedData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次进入英文安全路径模式时,将打包内预置数据库复制到安全数据目录。
|
||||
*/
|
||||
ensureSeedData() {
|
||||
const hasTargetData = fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0;
|
||||
if (hasTargetData) {
|
||||
this.log('system', '检测到已有 MySQL 安全数据目录,直接复用');
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDataPath = path.join(this.sourceMysqlPath, 'data');
|
||||
if (fs.existsSync(sourceDataPath) && fs.readdirSync(sourceDataPath).length > 0) {
|
||||
this.log('system', '首次启动,正在复制预置 MySQL 数据到安全数据目录...');
|
||||
this.copyDirectorySync(sourceDataPath, this.dataPath);
|
||||
this.log('success', '预置 MySQL 数据复制完成');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(this.dataPath, { recursive: true });
|
||||
this.log('warn', '未找到预置 MySQL 数据目录,后续将按空库初始化');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令并返回Promise
|
||||
*/
|
||||
@@ -352,7 +459,10 @@ default-character-set=utf8mb4
|
||||
* @returns {Promise<number>} 返回 MySQL 运行的端口
|
||||
*/
|
||||
async ensureServiceRunning(findAvailablePort, waitForPort) {
|
||||
this.ensureRuntimeEnvironment();
|
||||
|
||||
this.log('system', '开始 MySQL 进程检查流程(进程模式)');
|
||||
this.log('system', `路径策略: ${this.pathStrategy.usesSafePaths ? '英文安全路径模式' : '应用目录直启模式'}`);
|
||||
this.log('system', `MySQL 路径: ${this.mysqlPath}`);
|
||||
this.log('system', `配置文件: ${this.configFile}`);
|
||||
|
||||
@@ -401,4 +511,4 @@ default-character-set=utf8mb4
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MySQLServiceManager;
|
||||
module.exports = MySQLProcessManager;
|
||||
41
scripts/path-utils.js
Normal file
41
scripts/path-utils.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 判断路径中是否包含非 ASCII 字符。
|
||||
* 这里不只判断中文,其他非 ASCII 字符同样视为不安全路径。
|
||||
*/
|
||||
function hasNonAscii(targetPath = '') {
|
||||
return /[^\x00-\x7F]/.test(targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前路径所在盘符根目录,例如 D:\
|
||||
*/
|
||||
function getDriveRoot(targetPath = '') {
|
||||
const resolvedPath = path.resolve(targetPath || process.cwd());
|
||||
return path.parse(resolvedPath).root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析运行期路径策略。
|
||||
* - 安全路径:继续直接使用应用目录
|
||||
* - 非 ASCII 路径:切到英文安全路径
|
||||
*/
|
||||
function resolveRuntimeStrategy(baseDir) {
|
||||
const normalizedBaseDir = path.resolve(baseDir);
|
||||
const driveRoot = getDriveRoot(normalizedBaseDir);
|
||||
const usesSafePaths = hasNonAscii(normalizedBaseDir);
|
||||
|
||||
return {
|
||||
baseDir: normalizedBaseDir,
|
||||
driveRoot,
|
||||
usesSafePaths,
|
||||
safeRuntimeRoot: path.join(driveRoot, 'NPQS9100_Runtime')
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasNonAscii,
|
||||
getDriveRoot,
|
||||
resolveRuntimeStrategy
|
||||
};
|
||||
Reference in New Issue
Block a user