zhilian-auto-hi
Version:
智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具
613 lines (612 loc) • 22.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GracefulShutdown = void 0;
exports.safeCleanupStep = safeCleanupStep;
exports.extractErrorMessage = extractErrorMessage;
exports.withErrorHandling = withErrorHandling;
const types_1 = require("../types");
const logger_1 = __importDefault(require("../logger"));
/**
* 优雅关闭管理器
* 负责处理应用程序的优雅关闭流程,包括信号处理、资源清理和状态管理
*/
class GracefulShutdown {
constructor(options) {
this.browser = null;
this.cleanupCallbacks = [];
this.stats = null;
// 使用固定配置,不读取配置文件
this.options = {
timeout: 10000, // 固定10秒超时
signals: ['SIGINT', 'SIGTERM'], // 固定监听信号
showStats: true, // 固定显示统计
...options
};
this.state = {
isShuttingDown: false,
shutdownStartTime: null,
forceShutdownTimeout: null
};
this.setupSignalHandlers();
}
/**
* 注册浏览器实例
* @param browser Playwright浏览器实例
*/
registerBrowser(browser) {
this.browser = browser;
}
/**
* 注册清理回调函数
* @param callback 清理回调函数
*/
registerCleanupCallback(callback) {
this.cleanupCallbacks.push(callback);
}
/**
* 检查是否正在关闭
* @returns 是否正在关闭
*/
isShuttingDown() {
return this.state.isShuttingDown;
}
/**
* 获取关闭统计信息
* @returns 关闭统计信息
*/
getShutdownStats() {
return this.stats;
}
/**
* 设置处理统计信息
* @param processingStats 处理统计信息
*/
setProcessingStats(processingStats) {
if (this.stats) {
this.stats.processingStats = processingStats;
}
}
/**
* 启动优雅关闭流程
* @param signal 触发关闭的信号
*/
async initiateShutdown(signal) {
// 如果已经在关闭过程中,第二次信号将触发强制关闭
if (this.state.isShuttingDown) {
logger_1.default.forceShutdownWarning('repeated_signal');
this.forceShutdown();
return;
}
// 标记开始关闭
this.state.isShuttingDown = true;
this.state.shutdownStartTime = new Date();
// 初始化统计信息
this.stats = {
signal,
startTime: this.state.shutdownStartTime,
phase: types_1.ShutdownPhase.INITIATED,
cleanupSteps: [],
errors: []
};
// 显示关闭开始信息
logger_1.default.shutdownStart(signal);
// 设置强制关闭超时(10秒)
this.state.forceShutdownTimeout = setTimeout(() => {
logger_1.default.forceShutdownWarning('timeout');
this.forceShutdown();
}, this.options.timeout);
try {
// 执行清理操作
await this.performCleanup();
// 清理完成
this.stats.phase = types_1.ShutdownPhase.COMPLETED;
this.stats.endTime = new Date();
logger_1.default.shutdownComplete(this.stats);
// 清除超时定时器
if (this.state.forceShutdownTimeout) {
clearTimeout(this.state.forceShutdownTimeout);
this.state.forceShutdownTimeout = null;
}
// 正常退出
process.exit(0);
}
catch (error) {
// 如果清理过程中发生严重错误,也要强制关闭
logger_1.default.error(`清理过程中发生严重错误: ${error instanceof Error ? error.message : String(error)}`);
this.forceShutdown();
}
}
/**
* 执行清理操作
*/
async performCleanup() {
if (!this.stats)
return;
this.stats.phase = types_1.ShutdownPhase.CLEANING_UP;
// 定义清理步骤的执行顺序和重试策略
const cleanupSteps = [
{
name: '停止当前操作',
fn: async () => {
// 这里可以添加停止当前操作的逻辑
// 例如设置全局标志、取消正在进行的请求等
},
retries: 0 // 不重试,因为这是状态设置
},
{
name: '执行注册的清理回调',
fn: async () => {
// 并行执行所有清理回调,但每个回调都有独立的错误处理
const callbackPromises = this.cleanupCallbacks.map(async (callback, index) => {
try {
await this.executeCleanupWithTimeout(callback, 3000); // 每个回调3秒超时
}
catch (error) {
const errorMsg = this.extractErrorMessage(error);
if (this.stats) {
this.stats.errors.push(`清理回调 ${index + 1}: ${errorMsg}`);
}
logger_1.default.warning(`清理回调 ${index + 1} 执行失败: ${errorMsg}`);
}
});
await Promise.allSettled(callbackPromises);
},
retries: 1 // 重试一次
},
{
name: '关闭浏览器页面和上下文',
fn: async () => {
if (this.browser) {
try {
// 获取所有上下文
const contexts = this.browser.contexts();
// 关闭所有页面和上下文
for (const context of contexts) {
try {
const pages = context.pages();
// 关闭所有页面
await Promise.allSettled(pages.map(page => page.close().catch(() => { })));
// 关闭上下文
await context.close().catch(() => { });
}
catch (error) {
// 忽略单个上下文关闭的错误
}
}
}
catch (error) {
// 如果获取上下文失败,记录但继续
logger_1.default.warning(`获取浏览器上下文时出错: ${this.extractErrorMessage(error)}`);
}
}
},
retries: 2 // 重试两次
},
{
name: '关闭浏览器实例',
fn: async () => {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
},
retries: 2 // 重试两次
},
{
name: '清理临时资源',
fn: async () => {
// 清理可能的临时文件、缓存等
// 这里可以添加具体的临时资源清理逻辑
await this.cleanupTemporaryResources();
},
retries: 1 // 重试一次
}
];
// 按顺序执行清理步骤
for (const step of cleanupSteps) {
// 检查是否需要强制退出
if (!this.state.isShuttingDown) {
logger_1.default.warning('清理过程中检测到状态变化,停止清理');
break;
}
await this.safeCleanupStep(step.name, step.fn, step.retries);
}
// 最后的资源检查和强制清理
await this.safeCleanupStep('最终资源检查', async () => {
await this.finalResourceCheck();
});
}
/**
* 清理临时资源
*/
async cleanupTemporaryResources() {
// 这里可以添加清理临时文件、缓存、网络连接等的逻辑
// 目前主要是确保没有遗留的定时器或监听器
// 清理可能的定时器(除了我们自己的强制关闭定时器)
// 注意:这里不能清理所有定时器,因为可能影响其他模块
// 清理可能的事件监听器
// 移除我们添加的信号监听器以外的监听器
// 这是一个占位符方法,具体实现取决于应用程序的需求
}
/**
* 最终资源检查
*/
async finalResourceCheck() {
// 检查浏览器是否真的关闭了
if (this.browser) {
try {
// 尝试获取浏览器版本,如果失败说明已经关闭
await this.browser.version();
logger_1.default.warning('浏览器实例仍然活跃,尝试强制关闭');
await this.browser.close().catch(() => { });
}
catch (error) {
// 如果获取版本失败,说明浏览器已经关闭,这是正常的
}
}
// 检查是否有未完成的异步操作
// 这里可以添加更多的资源检查逻辑
}
/**
* 安全执行清理步骤
* @param stepName 步骤名称
* @param cleanupFn 清理函数
* @param retryCount 重试次数,默认为0(不重试)
*/
async safeCleanupStep(stepName, cleanupFn, retryCount = 0) {
if (!this.stats)
return;
logger_1.default.cleanupStepStart(stepName);
let lastError = null;
let attempt = 0;
const maxAttempts = retryCount + 1;
// 尝试执行清理步骤,支持重试
while (attempt < maxAttempts) {
attempt++;
try {
await this.executeCleanupWithTimeout(cleanupFn, 5000); // 5秒超时
// 执行成功
this.stats.cleanupSteps.push(stepName);
logger_1.default.cleanupStepComplete(stepName);
return;
}
catch (error) {
lastError = this.extractErrorMessage(error);
if (attempt < maxAttempts) {
logger_1.default.warning(`清理步骤失败,正在重试 (${attempt}/${maxAttempts}): ${stepName} - ${lastError}`);
// 重试前等待一小段时间
await this.delay(1000);
}
}
}
// 所有尝试都失败了
const errorDetail = `${stepName}: ${lastError} (尝试 ${maxAttempts} 次后失败)`;
this.stats.errors.push(errorDetail);
logger_1.default.cleanupStepFailed(stepName, lastError || '未知错误');
// 记录详细错误信息但不抛出异常,确保其他清理步骤能继续执行
this.logCleanupError(stepName, lastError, maxAttempts);
}
/**
* 带超时的清理函数执行
* @param cleanupFn 清理函数
* @param timeoutMs 超时时间(毫秒)
*/
async executeCleanupWithTimeout(cleanupFn, timeoutMs) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`清理操作超时 (${timeoutMs}ms)`));
}, timeoutMs);
cleanupFn()
.then(() => {
clearTimeout(timeoutId);
resolve();
})
.catch((error) => {
clearTimeout(timeoutId);
reject(error);
});
});
}
/**
* 提取错误消息
* @param error 错误对象
* @returns 错误消息字符串
*/
extractErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object') {
// 尝试提取常见的错误属性
const errorObj = error;
if (errorObj.message) {
return String(errorObj.message);
}
if (errorObj.code) {
return `错误代码: ${errorObj.code}`;
}
if (errorObj.name) {
return `错误类型: ${errorObj.name}`;
}
}
return String(error);
}
/**
* 记录清理错误的详细信息
* @param stepName 步骤名称
* @param error 错误信息
* @param attempts 尝试次数
*/
logCleanupError(stepName, error, attempts) {
// 这里可以添加更详细的错误日志记录
// 例如记录到文件、发送到监控系统等
console.error(`[GracefulShutdown] 清理步骤彻底失败: ${stepName}`);
console.error(`[GracefulShutdown] 错误详情: ${error || '未知错误'}`);
console.error(`[GracefulShutdown] 尝试次数: ${attempts}`);
console.error(`[GracefulShutdown] 时间戳: ${new Date().toISOString()}`);
}
/**
* 延迟函数
* @param ms 延迟毫秒数
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 强制关闭
*/
forceShutdown() {
if (this.stats) {
this.stats.phase = types_1.ShutdownPhase.FORCED;
this.stats.endTime = new Date();
}
// 清除超时定时器
if (this.state.forceShutdownTimeout) {
clearTimeout(this.state.forceShutdownTimeout);
this.state.forceShutdownTimeout = null;
}
// 尝试强制关闭浏览器进程
try {
this.forceBrowserTermination();
}
catch (error) {
// 记录强制关闭浏览器时的错误,但不阻止程序退出
const errorMsg = this.extractErrorMessage(error);
if (this.stats) {
this.stats.errors.push(`强制关闭浏览器失败: ${errorMsg}`);
}
logger_1.default.error(`强制关闭浏览器时出错: ${errorMsg}`);
}
// 执行最后的紧急清理
try {
this.emergencyCleanup();
}
catch (error) {
// 紧急清理失败也不能阻止程序退出
logger_1.default.error(`紧急清理失败: ${this.extractErrorMessage(error)}`);
}
// 显示强制关闭完成信息
if (this.stats) {
logger_1.default.shutdownComplete(this.stats);
}
// 强制退出进程
process.exit(1);
}
/**
* 强制终止浏览器进程
*/
forceBrowserTermination() {
if (!this.browser)
return;
const errors = [];
try {
// 尝试立即关闭浏览器,不等待
this.browser.close().catch((error) => {
errors.push(`浏览器关闭失败: ${this.extractErrorMessage(error)}`);
});
// 尝试获取浏览器进程信息并强制终止
try {
const browserProcess = this.browser._connection?._transport?._process;
if (browserProcess && browserProcess.pid) {
// 尝试不同的终止信号
const signals = ['SIGTERM', 'SIGKILL'];
for (const signal of signals) {
try {
process.kill(browserProcess.pid, signal);
logger_1.default.cleanupProgress(`发送 ${signal} 信号到浏览器进程 ${browserProcess.pid}`);
break; // 如果成功发送信号,就不需要尝试其他信号
}
catch (killError) {
errors.push(`发送 ${signal} 信号失败: ${this.extractErrorMessage(killError)}`);
}
}
}
else {
errors.push('无法获取浏览器进程信息');
}
}
catch (processError) {
errors.push(`获取浏览器进程时出错: ${this.extractErrorMessage(processError)}`);
}
// 尝试通过其他方式强制关闭
try {
// 如果浏览器有 _process 属性(某些版本的 Playwright)
const directProcess = this.browser._process;
if (directProcess && directProcess.pid && directProcess.kill) {
directProcess.kill('SIGKILL');
logger_1.default.cleanupProgress('通过直接进程引用强制终止浏览器');
}
}
catch (directKillError) {
errors.push(`直接终止进程失败: ${this.extractErrorMessage(directKillError)}`);
}
}
catch (error) {
errors.push(`强制终止浏览器时发生未预期错误: ${this.extractErrorMessage(error)}`);
}
// 记录所有错误,但不抛出异常
if (errors.length > 0 && this.stats) {
this.stats.errors.push(...errors);
}
// 清空浏览器引用
this.browser = null;
}
/**
* 紧急清理 - 在强制关闭前执行的最后清理操作
*/
emergencyCleanup() {
try {
// 清理所有定时器(除了Node.js内部的)
// 注意:这是一个激进的操作,只在紧急情况下使用
// 清理可能的全局引用
if (global && global.browser) {
global.browser = null;
}
// 清理可能的进程监听器(除了我们自己的)
// 注意:这里要小心,不要移除重要的系统监听器
// 强制垃圾回收(如果可用)
if (global.gc) {
try {
global.gc();
logger_1.default.cleanupProgress('执行强制垃圾回收');
}
catch (gcError) {
// 忽略垃圾回收错误
}
}
logger_1.default.cleanupProgress('紧急清理完成');
}
catch (error) {
// 紧急清理失败不应该阻止程序退出
logger_1.default.error(`紧急清理过程中出错: ${this.extractErrorMessage(error)}`);
}
}
/**
* 设置信号监听器
*/
setupSignalHandlers() {
for (const signal of this.options.signals) {
process.on(signal, () => {
this.initiateShutdown(signal);
});
}
}
}
exports.GracefulShutdown = GracefulShutdown;
/**
* 通用的安全清理步骤包装器函数
* 可以被其他模块使用来安全地执行清理操作
*
* @param stepName 步骤名称
* @param cleanupFn 清理函数
* @param options 选项
* @returns Promise<boolean> 返回是否成功执行
*/
async function safeCleanupStep(stepName, cleanupFn, options = {}) {
const { retries = 0, timeoutMs = 5000, logErrors = true } = options;
let lastError = null;
let attempt = 0;
const maxAttempts = retries + 1;
while (attempt < maxAttempts) {
attempt++;
try {
// 带超时的执行
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`清理操作超时 (${timeoutMs}ms)`));
}, timeoutMs);
cleanupFn()
.then(() => {
clearTimeout(timeoutId);
resolve();
})
.catch((error) => {
clearTimeout(timeoutId);
reject(error);
});
});
// 执行成功
if (logErrors) {
logger_1.default.cleanupStepComplete(stepName);
}
return true;
}
catch (error) {
lastError = extractErrorMessage(error);
if (attempt < maxAttempts) {
if (logErrors) {
logger_1.default.warning(`清理步骤失败,正在重试 (${attempt}/${maxAttempts}): ${stepName} - ${lastError}`);
}
// 重试前等待一小段时间
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
// 所有尝试都失败了
if (logErrors) {
logger_1.default.cleanupStepFailed(stepName, lastError || '未知错误');
}
return false;
}
/**
* 提取错误消息的工具函数
* @param error 错误对象
* @returns 错误消息字符串
*/
function extractErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object') {
// 尝试提取常见的错误属性
const errorObj = error;
if (errorObj.message) {
return String(errorObj.message);
}
if (errorObj.code) {
return `错误代码: ${errorObj.code}`;
}
if (errorObj.name) {
return `错误类型: ${errorObj.name}`;
}
}
return String(error);
}
/**
* 创建一个带错误处理的异步函数包装器
* @param fn 要包装的函数
* @param errorHandler 错误处理函数
* @returns 包装后的函数
*/
function withErrorHandling(fn, errorHandler) {
return async (...args) => {
try {
return await fn(...args);
}
catch (error) {
if (errorHandler) {
try {
await errorHandler(error, ...args);
}
catch (handlerError) {
logger_1.default.error(`错误处理器本身出错: ${extractErrorMessage(handlerError)}`);
}
}
else {
logger_1.default.error(`函数执行出错: ${extractErrorMessage(error)}`);
}
return null;
}
};
}
// 导出单例实例
exports.default = new GracefulShutdown();