修复Windows维护页门禁执行
转换WSL bash所需的脚本、参数和环境路径 在POSIX平台校验实际0644权限并在Windows校验安装源码契约 保留维护页原子替换与符号链接防护测试
This commit is contained in:
@@ -19,7 +19,14 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
);
|
||||
const bashExecutable =
|
||||
process.platform === 'win32'
|
||||
? 'C:\\Program Files\\Git\\bin\\bash.exe'
|
||||
: 'bash';
|
||||
const failures = [];
|
||||
const requestedFiles = [];
|
||||
|
||||
@@ -48,7 +55,10 @@ function validateDefaultPage(filePath) {
|
||||
}
|
||||
for (const [pattern, label] of [
|
||||
[/(?:今天|今晚|明天|昨天|昨日)/u, '相对日期'],
|
||||
[/(?:20\d{2}[-/.年]\d{1,2}(?:[-/.月]\d{1,2}日?)?|\d{1,2}月\d{1,2}日)/u, '具体日期'],
|
||||
[
|
||||
/(?:20\d{2}[-/.年]\d{1,2}(?:[-/.月]\d{1,2}日?)?|\d{1,2}月\d{1,2}日)/u,
|
||||
'具体日期',
|
||||
],
|
||||
[/(?:[01]?\d|2[0-3]):[0-5]\d/u, '具体维护时间'],
|
||||
]) {
|
||||
if (pattern.test(source)) {
|
||||
@@ -58,15 +68,48 @@ function validateDefaultPage(filePath) {
|
||||
}
|
||||
|
||||
function runScript(scriptPath, args, env) {
|
||||
return spawnSync('bash', [scriptPath, ...args], {
|
||||
const bashArgs = args.map((arg) =>
|
||||
path.isAbsolute(arg) ? toBashPath(arg) : arg,
|
||||
);
|
||||
const bashEnv = Object.fromEntries(
|
||||
Object.entries(env).map(([key, value]) => [
|
||||
key,
|
||||
path.isAbsolute(value) ? toBashPath(value) : value,
|
||||
]),
|
||||
);
|
||||
const envAssignments = Object.entries(bashEnv).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
);
|
||||
const commandArgs = [
|
||||
...envAssignments,
|
||||
'bash',
|
||||
toBashPath(scriptPath),
|
||||
...bashArgs,
|
||||
];
|
||||
const command = `exec env ${commandArgs.map(shellQuote).join(' ')}`;
|
||||
return spawnSync(bashExecutable, ['-c', command], {
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env, ...env },
|
||||
env: process.env,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function toBashPath(filePath) {
|
||||
const windowsDrive = /^([A-Za-z]):[\\/](.*)$/u.exec(filePath);
|
||||
if (windowsDrive) {
|
||||
return `/${windowsDrive[1].toLowerCase()}/${windowsDrive[2].replaceAll('\\', '/')}`;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function validateRuntimePageLifecycle() {
|
||||
const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'genarrative-maintenance-'));
|
||||
const tempRoot = mkdtempSync(
|
||||
path.join(os.tmpdir(), 'genarrative-maintenance-'),
|
||||
);
|
||||
const markerFile = path.join(tempRoot, 'state', 'enabled');
|
||||
const runtimePageFile = path.join(tempRoot, 'state', 'page.html');
|
||||
const sourcePageFile = path.join(tempRoot, 'announcement.html');
|
||||
@@ -81,6 +124,9 @@ function validateRuntimePageLifecycle() {
|
||||
if (!onScriptSource.includes('replace_file_atomically')) {
|
||||
fail('maintenance-on 必须通过统一 helper 原子替换公告页和 marker。');
|
||||
}
|
||||
if (!onScriptSource.includes('install -m 0644')) {
|
||||
fail('maintenance-on 安装运行态公告页时必须显式设置 0644 权限。');
|
||||
}
|
||||
if (/\bmv\s+-[^\s]*T\b/u.test(onScriptSource)) {
|
||||
fail('maintenance-on 不得使用 GNU mv 专属的 -T 参数。');
|
||||
}
|
||||
@@ -95,7 +141,9 @@ function validateRuntimePageLifecycle() {
|
||||
env,
|
||||
);
|
||||
if (enable.status !== 0) {
|
||||
fail(`maintenance-on --page-file 执行失败: ${enable.stderr || enable.stdout}`);
|
||||
fail(
|
||||
`maintenance-on --page-file 执行失败: ${enable.stderr || enable.stdout}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!existsSync(markerFile)) {
|
||||
@@ -107,7 +155,10 @@ function validateRuntimePageLifecycle() {
|
||||
if (readFileSync(runtimePageFile, 'utf8') !== announcement) {
|
||||
fail('运行态公告页内容与输入文件不一致。');
|
||||
}
|
||||
if ((statSync(runtimePageFile).mode & 0o777) !== 0o644) {
|
||||
if (
|
||||
process.platform !== 'win32' &&
|
||||
(statSync(runtimePageFile).mode & 0o777) !== 0o644
|
||||
) {
|
||||
fail('运行态公告页权限必须为 0644。');
|
||||
}
|
||||
}
|
||||
@@ -130,7 +181,9 @@ function validateRuntimePageLifecycle() {
|
||||
chmodSync(runtimePageFile, 0o644);
|
||||
const genericEnable = runScript(onScript, ['generic maintenance'], env);
|
||||
if (genericEnable.status !== 0) {
|
||||
fail(`通用 maintenance-on 执行失败: ${genericEnable.stderr || genericEnable.stdout}`);
|
||||
fail(
|
||||
`通用 maintenance-on 执行失败: ${genericEnable.stderr || genericEnable.stdout}`,
|
||||
);
|
||||
}
|
||||
if (existsSync(runtimePageFile)) {
|
||||
fail('新维护窗口未提供 --page-file 时必须清理残留公告页。');
|
||||
@@ -265,10 +318,9 @@ function validateGatewayConfiguration() {
|
||||
}
|
||||
}
|
||||
|
||||
for (const filePath of
|
||||
requestedFiles.length > 0
|
||||
? requestedFiles
|
||||
: [path.join(repoRoot, 'public/maintenance.html')]) {
|
||||
for (const filePath of requestedFiles.length > 0
|
||||
? requestedFiles
|
||||
: [path.join(repoRoot, 'public/maintenance.html')]) {
|
||||
validateDefaultPage(filePath);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user