using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PuppeteerSharp; using HtmlToPdfService.Core.Options; namespace HtmlToPdfService.Core.Pool; /// /// 浏览器池实现 /// public class BrowserPool : IBrowserPool { private readonly BrowserPoolOptions _options; private readonly ILogger _logger; private readonly ConcurrentBag _availableBrowsers = new(); private readonly ConcurrentDictionary _allBrowsers = new(); private readonly SemaphoreSlim _semaphore; private readonly SemaphoreSlim _createLock = new(1, 1); private int _totalInstances = 0; private bool _disposed = false; public BrowserPool(IOptions options, ILogger logger) { _options = options.Value.BrowserPool; _logger = logger; _semaphore = new SemaphoreSlim(_options.MaxConcurrent, _options.MaxConcurrent); } /// /// 获取浏览器实例 /// public async Task AcquireAsync(CancellationToken cancellationToken = default) { if (_disposed) throw new ObjectDisposedException(nameof(BrowserPool)); // 等待信号量(控制并发数) var acquired = await _semaphore.WaitAsync(_options.AcquireTimeout, cancellationToken); if (!acquired) { _logger.LogWarning("获取浏览器实例超时,当前并发数已达上限"); throw new TimeoutException($"无法在 {_options.AcquireTimeout}ms 内获取浏览器实例"); } try { // 尝试从池中获取可用实例 while (_availableBrowsers.TryTake(out var browser)) { if (browser.IsConnected) { _logger.LogDebug("复用浏览器实例,当前总实例数: {Total}", _totalInstances); return browser; } else { // 实例已失效,清理 _logger.LogWarning("检测到断开的浏览器实例,正在清理"); await DisposeBrowserAsync(browser); } } // 池中没有可用实例,创建新实例 if (_totalInstances < _options.MaxInstances) { var newBrowser = await CreateBrowserAsync(cancellationToken); _logger.LogInformation("创建新浏览器实例,当前总实例数: {Total}/{Max}", _totalInstances, _options.MaxInstances); return newBrowser; } // 达到最大实例数,等待其他任务释放 _logger.LogWarning("浏览器实例数已达上限 ({Max}),等待其他任务释放", _options.MaxInstances); throw new InvalidOperationException($"浏览器实例数已达上限: {_options.MaxInstances}"); } catch { _semaphore.Release(); throw; } } /// /// 释放浏览器实例 /// public void Release(IBrowser browser) { if (browser == null) throw new ArgumentNullException(nameof(browser)); try { if (browser.IsConnected && !_disposed) { _availableBrowsers.Add(browser); _logger.LogDebug("浏览器实例已归还到池中"); } else { _logger.LogWarning("释放的浏览器实例已断开或池已销毁"); _ = DisposeBrowserAsync(browser); // 异步清理,不等待 } } finally { _semaphore.Release(); } } /// /// 预热浏览器池 /// public async Task WarmUpAsync(CancellationToken cancellationToken = default) { _logger.LogInformation("开始预热浏览器池,目标实例数: {Min}", _options.MinInstances); var tasks = new List(); for (int i = 0; i < _options.MinInstances; i++) { tasks.Add(Task.Run(async () => { try { var browser = await CreateBrowserAsync(cancellationToken); _availableBrowsers.Add(browser); _logger.LogDebug("预热:创建浏览器实例 {Current}/{Target}", i + 1, _options.MinInstances); } catch (Exception ex) { _logger.LogError(ex, "预热失败:创建浏览器实例时出错"); } }, cancellationToken)); } await Task.WhenAll(tasks); _logger.LogInformation("浏览器池预热完成,当前实例数: {Total}", _totalInstances); } /// /// 获取池状态 /// public BrowserPoolStatus GetStatus() { return new BrowserPoolStatus { TotalInstances = _totalInstances, AvailableInstances = _availableBrowsers.Count, MaxInstances = _options.MaxInstances, InUseInstances = _totalInstances - _availableBrowsers.Count }; } /// /// 创建浏览器实例 /// private async Task CreateBrowserAsync(CancellationToken cancellationToken) { await _createLock.WaitAsync(cancellationToken); try { // 双重检查,避免超过最大实例数 if (_totalInstances >= _options.MaxInstances) { throw new InvalidOperationException($"浏览器实例数已达上限: {_options.MaxInstances}"); } // 确保 Chromium 已下载,并获取可执行文件路径 var chromiumPath = await EnsureChromiumDownloadedAsync(); // 使用配置的路径,如果为空则使用下载的路径 var executablePath = string.IsNullOrWhiteSpace(_options.ExecutablePath) ? chromiumPath : _options.ExecutablePath; _logger.LogDebug("使用的 Chromium 路径: {Path}", executablePath); var launchOptions = new LaunchOptions { Headless = true, Args = _options.BrowserArgs, ExecutablePath = executablePath }; _logger.LogDebug("启动参数: Headless={Headless}, Args={Args}", launchOptions.Headless, string.Join(" ", launchOptions.Args ?? Array.Empty())); var browser = await Puppeteer.LaunchAsync(launchOptions); var browserId = Guid.NewGuid().ToString(); _allBrowsers.TryAdd(browserId, browser); Interlocked.Increment(ref _totalInstances); _logger.LogDebug("成功创建浏览器实例: {BrowserId}", browserId); return browser; } catch (Exception ex) { _logger.LogError(ex, "创建浏览器实例失败"); throw; } finally { _createLock.Release(); } } /// /// 清理浏览器实例 /// private async Task DisposeBrowserAsync(IBrowser browser) { try { await browser.DisposeAsync(); Interlocked.Decrement(ref _totalInstances); // 从字典中移除 var key = _allBrowsers.FirstOrDefault(x => x.Value == browser).Key; if (key != null) { _allBrowsers.TryRemove(key, out _); } _logger.LogDebug("浏览器实例已清理,当前总实例数: {Total}", _totalInstances); } catch (Exception ex) { _logger.LogError(ex, "清理浏览器实例时出错"); } } /// /// 确保 Chromium 已下载,并返回可执行文件路径 /// private async Task EnsureChromiumDownloadedAsync() { try { _logger.LogInformation("检查 Chromium 是否已下载..."); var browserFetcher = new BrowserFetcher(); // 检查是否已经下载 var installedBrowsers = browserFetcher.GetInstalledBrowsers(); if (installedBrowsers.Any()) { var installedBrowser = installedBrowsers.First(); var executablePath = installedBrowser.GetExecutablePath(); _logger.LogInformation("Chromium 已存在,版本: {Revision},路径: {Path}", installedBrowser.BuildId, executablePath); return executablePath; } // 下载 Chromium,启动进度提示 _logger.LogInformation("========================================"); _logger.LogInformation("🔽 开始下载 Chromium(约 300MB)"); _logger.LogInformation("⏱️ 预计需要 2-10 分钟,取决于网络速度"); _logger.LogInformation("========================================"); // 启动一个后台任务来显示"下载中"的提示 var cts = new CancellationTokenSource(); var progressTask = Task.Run(async () => { int dots = 0; var messages = new[] { "📥 正在下载 Chromium,请稍候", "⏳ 继续下载中,请耐心等待", "🚀 下载进行中,马上就好", "💪 坚持住,快完成了" }; int messageIndex = 0; while (!cts.Token.IsCancellationRequested) { await Task.Delay(5000, cts.Token); if (!cts.Token.IsCancellationRequested) { _logger.LogInformation("{Message}{Dots}", messages[messageIndex % messages.Length], new string('.', (dots % 3) + 1)); dots++; if (dots % 3 == 0) messageIndex++; } } }, cts.Token); try { var installedBrowser = await browserFetcher.DownloadAsync(); cts.Cancel(); var executablePath = installedBrowser.GetExecutablePath(); _logger.LogInformation("========================================"); _logger.LogInformation("✅ Chromium 下载完成!"); _logger.LogInformation("📁 路径: {Path}", executablePath); _logger.LogInformation("🏷️ 版本: {Revision}", installedBrowser.BuildId); _logger.LogInformation("========================================"); return executablePath; } catch { cts.Cancel(); throw; } } catch (Exception ex) { _logger.LogError(ex, "下载 Chromium 失败"); throw new InvalidOperationException("无法下载 Chromium。请检查网络连接或手动下载。", ex); } } /// /// 释放资源 /// public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; _logger.LogInformation("开始清理浏览器池..."); // 清理所有浏览器实例 var disposeTasks = _allBrowsers.Values.Select(async browser => { try { await browser.DisposeAsync(); } catch (Exception ex) { _logger.LogError(ex, "清理浏览器实例时出错"); } }); await Task.WhenAll(disposeTasks); _allBrowsers.Clear(); _availableBrowsers.Clear(); _totalInstances = 0; _semaphore.Dispose(); _createLock.Dispose(); _logger.LogInformation("浏览器池清理完成"); } }