UNPKG

zhilian-auto-hi

Version:

智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具

613 lines (612 loc) 22.8 kB
"use strict"; 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();