Files
reverse-skills-max/vendor-new/xbsReverseSkill/web-js-env-patcher/scripts/capture_ruyitrace_log.js

285 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
function parseArgs(argv) {
const args = {
url: '',
caseDir: 'case',
outDir: '',
profileDir: '',
ruyitraceHome: '',
ruyitraceExe: '',
duration: 60,
limit: 200000,
dryRun: false,
importAfter: false,
json: false,
markdown: false,
};
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--url') args.url = argv[++i] || '';
else if (a === '--case-dir' || a === '--dir') args.caseDir = argv[++i] || '';
else if (a === '--out-dir') args.outDir = argv[++i] || '';
else if (a === '--profile-dir') args.profileDir = argv[++i] || '';
else if (a === '--ruyitrace-home') args.ruyitraceHome = argv[++i] || '';
else if (a === '--ruyitrace-exe') args.ruyitraceExe = argv[++i] || '';
else if (a === '--duration') args.duration = Number(argv[++i] || '60');
else if (a === '--limit') args.limit = Number(argv[++i] || '200000');
else if (a === '--dry-run') args.dryRun = true;
else if (a === '--import-after') args.importAfter = true;
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!args.json && !args.markdown) args.markdown = true;
if (!Number.isFinite(args.duration) || args.duration <= 0) args.duration = 60;
if (!Number.isFinite(args.limit) || args.limit <= 0) args.limit = 200000;
return args;
}
function usage() {
return `用法:
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --ruyitrace-home <RuyiTrace-dir> --markdown
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --duration 90 --import-after --markdown
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --dry-run --json
说明:确认 RuyiTrace 已安装后,优先使用随 RuyiTrace 提供的 trace Firefox 和 MOZ_DOM_TRACE 环境变量自动捕获 NDJSON。自动捕获失败、需要登录/验证码/权限交互、或目标路径未覆盖时,才要求用户手动协助。`;
}
function exists(p) {
try { return !!p && fs.existsSync(p); } catch { return false; }
}
function isDir(p) {
try { return !!p && fs.statSync(p).isDirectory(); } catch { return false; }
}
function ensureDir(p) {
fs.mkdirSync(p, { recursive: true });
}
function run(cmd, args, timeout = 8000) {
const ret = spawnSync(cmd, args, { encoding: 'utf8', timeout, windowsHide: true });
return { ok: ret.status === 0, stdout: ret.stdout || '', stderr: ret.stderr || '' };
}
function whereCommand(name) {
const cmd = process.platform === 'win32' ? 'where' : 'which';
const ret = run(cmd, [name]);
return ret.ok ? ret.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) : [];
}
function normalizeTraceHome(args) {
if (args.ruyitraceHome) return path.resolve(args.ruyitraceHome);
if (args.ruyitraceExe) return path.dirname(path.resolve(args.ruyitraceExe));
if (process.env.RUYI_TRACE_HOME) return path.resolve(process.env.RUYI_TRACE_HOME);
if (process.env.RUYITRACE_HOME) return path.resolve(process.env.RUYITRACE_HOME);
const found = whereCommand(process.platform === 'win32' ? 'RuyiTrace.exe' : 'RuyiTrace');
return found.length ? path.dirname(found[0]) : '';
}
function detectRuyiTrace(args) {
const home = normalizeTraceHome(args);
const exeName = process.platform === 'win32' ? 'RuyiTrace.exe' : 'RuyiTrace';
const exe = args.ruyitraceExe ? path.resolve(args.ruyitraceExe) : (home ? path.join(home, exeName) : '');
const firefoxExe = home ? path.join(home, 'firefox', process.platform === 'win32' ? 'firefox.exe' : 'firefox') : '';
const marker = home ? path.join(home, 'firefox', 'RUYI_DOMTRACE.txt') : '';
const installed = exists(exe) && exists(firefoxExe) && exists(marker);
return {
installed,
home,
exe,
exeExists: exists(exe),
firefoxExe,
firefoxExists: exists(firefoxExe),
marker,
markerExists: exists(marker),
reason: installed ? '' : 'RuyiTrace 不完整:需要 RuyiTrace 可执行文件、firefox 可执行文件以及 firefox/RUYI_DOMTRACE.txt',
};
}
function timestamp() {
return new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
}
function buildPlan(args, trace) {
const caseDir = path.resolve(args.caseDir || 'case');
const outDir = path.resolve(args.outDir || path.join(caseDir, 'ruyi-trace', 'logs'));
const profileDir = path.resolve(args.profileDir || path.join(caseDir, 'tmp', 'ruyitrace-profile'));
const traceFile = path.join(outDir, `trace-${timestamp()}.ndjson`);
const firefoxArgs = ['-no-remote', '-new-instance', '-profile', profileDir];
if (args.url) firefoxArgs.push(args.url);
return {
caseDir,
outDir,
profileDir,
traceFile,
firefoxExe: trace.firefoxExe,
firefoxArgs,
env: {
MOZ_DOM_TRACE: '1',
MOZ_DOM_TRACE_FILE: traceFile,
MOZ_DOM_TRACE_LIMIT: String(args.limit),
MOZ_DISABLE_LAUNCHER_PROCESS: '1',
},
};
}
function listNdjsonFiles(dir, sinceMs) {
if (!isDir(dir)) return [];
return fs.readdirSync(dir)
.filter((name) => name.toLowerCase().endsWith('.ndjson'))
.map((name) => path.join(dir, name))
.filter((file) => {
try { return fs.statSync(file).mtimeMs >= sinceMs - 1000; } catch { return false; }
})
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = (value) => {
if (done) return;
done = true;
resolve(value);
};
child.once('exit', (code, signal) => finish({ exited: true, code, signal }));
setTimeout(() => finish({ exited: false, code: null, signal: null }), timeoutMs).unref();
});
}
function importLog(caseDir, file, markdown) {
const script = path.join(__dirname, 'import_ruyitrace_log.js');
const args = [script, '--input', file, '--case-dir', caseDir, '--truncation-threshold', '3900', markdown ? '--markdown' : '--json'];
const ret = spawnSync(process.execPath, args, { encoding: 'utf8', windowsHide: true });
return { ok: ret.status === 0, status: ret.status, stdout: ret.stdout || '', stderr: ret.stderr || '' };
}
async function capture(args, plan) {
ensureDir(plan.outDir);
ensureDir(plan.profileDir);
const startedAt = Date.now();
const child = spawn(plan.firefoxExe, plan.firefoxArgs, {
env: { ...process.env, ...plan.env },
stdio: 'ignore',
windowsHide: false,
});
const result = {
launched: true,
pid: child.pid,
waitedSeconds: args.duration,
killAttempted: false,
exit: null,
logs: [],
importResult: null,
};
child.on('error', (err) => { result.launchError = err.message || String(err); });
await wait(args.duration * 1000);
const exitBeforeKill = await waitForExit(child, 200);
if (!exitBeforeKill.exited) {
result.killAttempted = true;
try { child.kill(); } catch (err) { result.killError = err.message || String(err); }
result.exit = await waitForExit(child, 3000);
} else {
result.exit = exitBeforeKill;
}
result.logs = listNdjsonFiles(plan.outDir, startedAt);
if (args.importAfter && result.logs.length) {
result.importResult = importLog(plan.caseDir, result.logs[0], args.markdown);
}
return result;
}
function renderMarkdown(obj) {
const { args, trace, plan, result } = obj;
const lines = ['# RuyiTrace 自动捕获日志', ''];
lines.push(`- RuyiTrace 检测结果:${trace.installed ? '通过' : '不通过'}`);
if (trace.home) lines.push(`- RuyiTrace 目录:${trace.home}`);
if (trace.exe) lines.push(`- RuyiTrace 可执行文件:${trace.exeExists ? '存在' : '不存在'} - ${trace.exe}`);
if (trace.firefoxExe) lines.push(`- trace Firefox:${trace.firefoxExists ? '存在' : '不存在'} - ${trace.firefoxExe}`);
if (trace.marker) lines.push(`- trace 标志文件:${trace.markerExists ? '存在' : '不存在'} - ${trace.marker}`);
if (trace.reason) lines.push(`- 原因:${trace.reason}`);
lines.push('', '## 自动捕获计划');
lines.push(`- 目标页面:${args.url || '未提供'}`);
lines.push(`- 输出目录:${plan.outDir}`);
lines.push(`- Profile 目录:${plan.profileDir}`);
lines.push(`- 计划 trace 文件:${plan.traceFile}`);
lines.push(`- 采集时长:${args.duration} 秒`);
lines.push(`- DOM trace 行数上限:${args.limit}`);
lines.push(`- 启动参数:${[plan.firefoxExe].concat(plan.firefoxArgs).join(' ')}`);
lines.push('- 环境变量:MOZ_DOM_TRACE=1,MOZ_DOM_TRACE_FILE=<case trace file>,MOZ_DOM_TRACE_LIMIT=<limit>,MOZ_DISABLE_LAUNCHER_PROCESS=1');
if (args.dryRun) {
lines.push('', '## Dry-run 结果');
lines.push('- 未启动浏览器,未创建日志文件。');
if (trace.installed) {
lines.push('- RuyiTrace 检测通过且用户授权后,应优先执行本脚本自动捕获 NDJSON,而不是默认等待用户手动 trace。');
} else {
lines.push('- RuyiTrace 检测未通过,不能进入自动捕获;应先让用户安装 / 提供 RuyiTrace 路径,或明确确认降级为仅 ruyiPage。');
}
return lines.join('\n') + '\n';
}
lines.push('', '## 捕获结果');
if (result.launchError) lines.push(`- 启动错误:${result.launchError}`);
lines.push(`- 是否已启动:${result.launched ? '是' : '否'}`);
if (result.pid) lines.push(`- 进程 PID:${result.pid}`);
lines.push(`- 是否尝试结束进程:${result.killAttempted ? '是' : '否'}`);
lines.push(`- 发现 NDJSON 数量:${result.logs.length}`);
for (const file of result.logs) lines.push(` - ${file}`);
if (!result.logs.length) {
lines.push('- 未发现 NDJSON:应检查 RuyiTrace trace Firefox 是否能写入日志、目标页面是否触发了环境访问、是否需要登录/验证码/权限交互;自动捕获失败后才要求用户手动协助采集。');
}
if (result.importResult) {
lines.push('', '## 导入结果');
lines.push(`- 导入是否成功:${result.importResult.ok ? '是' : '否'}`);
if (result.importResult.stdout.trim()) lines.push('', '```text', result.importResult.stdout.trim(), '```');
if (result.importResult.stderr.trim()) lines.push('', '```text', result.importResult.stderr.trim(), '```');
}
return lines.join('\n') + '\n';
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log(usage());
return;
}
if (!args.url) throw new Error('缺少 --url;自动捕获需要目标页面 URL。');
const trace = detectRuyiTrace(args);
const plan = buildPlan(args, trace);
if (!trace.installed) {
const obj = { args, trace, plan, result: { launched: false, logs: [] } };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
process.exitCode = 2;
return;
}
if (args.dryRun) {
const obj = { args, trace, plan, result: { launched: false, logs: [] } };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
return;
}
const result = await capture(args, plan);
const obj = { args, trace, plan, result };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
if (!result.logs.length) process.exitCode = 3;
}
main().catch((err) => {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
});