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("浏览器池清理完成");
}
}