|
@@ -361,16 +361,22 @@ class ExamPdfExportService
|
|
|
} finally {
|
|
} finally {
|
|
|
// 【修复】优化临时文件清理逻辑:
|
|
// 【修复】优化临时文件清理逻辑:
|
|
|
// 1. 合并失败时不删除源文件,便于重试
|
|
// 1. 合并失败时不删除源文件,便于重试
|
|
|
- // 2. 合并成功后才删除源文件
|
|
|
|
|
- // 3. 保留合并后的文件一段时间,便于调试
|
|
|
|
|
|
|
+ // 2. 合并成功后不立即删除源文件,保留2小时用于调试
|
|
|
|
|
+ // 3. 保留合并后的文件30分钟用于调试
|
|
|
|
|
|
|
|
if ($mergeSuccess && $uploadSuccess) {
|
|
if ($mergeSuccess && $uploadSuccess) {
|
|
|
- // 合并成功且上传成功,删除源文件
|
|
|
|
|
|
|
+ // 【优化】合并成功且上传成功,不立即删除源文件
|
|
|
|
|
+ // 改为设置未来删除时间,让源文件保留2小时
|
|
|
$sourceFiles = [$examPdfPath, $gradingPdfPath];
|
|
$sourceFiles = [$examPdfPath, $gradingPdfPath];
|
|
|
foreach ($sourceFiles as $file) {
|
|
foreach ($sourceFiles as $file) {
|
|
|
if ($file && file_exists($file)) {
|
|
if ($file && file_exists($file)) {
|
|
|
- @unlink($file);
|
|
|
|
|
- Log::debug('删除源PDF文件', ['path' => $file]);
|
|
|
|
|
|
|
+ // 设置2小时后删除
|
|
|
|
|
+ $deletionTime = time() + 7200; // 2小时 = 7200秒
|
|
|
|
|
+ @touch($file, $deletionTime);
|
|
|
|
|
+ Log::info('源PDF文件将在2小时后自动删除', [
|
|
|
|
|
+ 'path' => $file,
|
|
|
|
|
+ 'deletion_time' => date('Y-m-d H:i:s', $deletionTime)
|
|
|
|
|
+ ]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1246,6 +1252,12 @@ class ExamPdfExportService
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ Log::info('ExamPdfExportService: 开始Chrome渲染', [
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ 'tmp_pdf' => $tmpPdf,
|
|
|
|
|
+ 'chrome_binary' => $chromeBinary
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
// 设置运行时目录
|
|
// 设置运行时目录
|
|
|
$runtimeHome = sys_get_temp_dir() . '/chrome-home';
|
|
$runtimeHome = sys_get_temp_dir() . '/chrome-home';
|
|
|
$runtimeXdg = sys_get_temp_dir() . '/chrome-xdg';
|
|
$runtimeXdg = sys_get_temp_dir() . '/chrome-xdg';
|
|
@@ -1297,40 +1309,84 @@ class ExamPdfExportService
|
|
|
'XDG_RUNTIME_DIR' => $runtimeXdg,
|
|
'XDG_RUNTIME_DIR' => $runtimeXdg,
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- $process->setTimeout(60);
|
|
|
|
|
|
|
+ // 【修复】减少超时时间,避免队列任务整体超时
|
|
|
|
|
+ // 队列任务默认60秒超时,给Chrome进程设置45秒超时
|
|
|
|
|
+ $process->setTimeout(45);
|
|
|
$killSignal = \defined('SIGKILL') ? \SIGKILL : 9;
|
|
$killSignal = \defined('SIGKILL') ? \SIGKILL : 9;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
$startedAt = microtime(true);
|
|
$startedAt = microtime(true);
|
|
|
|
|
+ Log::info('ExamPdfExportService: Chrome进程启动', [
|
|
|
|
|
+ 'start_time' => date('Y-m-d H:i:s', (int)$startedAt),
|
|
|
|
|
+ 'timeout' => 45
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
$process->start();
|
|
$process->start();
|
|
|
$pdfGenerated = false;
|
|
$pdfGenerated = false;
|
|
|
|
|
|
|
|
- // 轮询检测PDF是否生成
|
|
|
|
|
|
|
+ // 【修复】缩短轮询时间,提高响应速度
|
|
|
$pollStart = microtime(true);
|
|
$pollStart = microtime(true);
|
|
|
- $maxPollSeconds = 30;
|
|
|
|
|
|
|
+ $maxPollSeconds = 40; // 缩短到40秒(小于Chrome超时45秒)
|
|
|
|
|
+ $checkInterval = 100_000; // 100ms检查一次
|
|
|
|
|
+
|
|
|
while ($process->isRunning() && (microtime(true) - $pollStart) < $maxPollSeconds) {
|
|
while ($process->isRunning() && (microtime(true) - $pollStart) < $maxPollSeconds) {
|
|
|
if (file_exists($tmpPdf) && filesize($tmpPdf) > 0) {
|
|
if (file_exists($tmpPdf) && filesize($tmpPdf) > 0) {
|
|
|
$pdfGenerated = true;
|
|
$pdfGenerated = true;
|
|
|
- $process->stop(5, $killSignal);
|
|
|
|
|
|
|
+ Log::info('ExamPdfExportService: PDF文件已生成,提前终止Chrome', [
|
|
|
|
|
+ 'elapsed' => round(microtime(true) - $startedAt, 2) . 's',
|
|
|
|
|
+ 'pdf_size' => filesize($tmpPdf)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $process->stop(2, $killSignal);
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
- usleep(200_000);
|
|
|
|
|
|
|
+ usleep($checkInterval);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ $elapsed = microtime(true) - $startedAt;
|
|
|
|
|
+ Log::info('ExamPdfExportService: Chrome轮询结束', [
|
|
|
|
|
+ 'elapsed' => round($elapsed, 2) . 's',
|
|
|
|
|
+ 'is_running' => $process->isRunning(),
|
|
|
|
|
+ 'pdf_exists' => file_exists($tmpPdf),
|
|
|
|
|
+ 'pdf_size' => file_exists($tmpPdf) ? filesize($tmpPdf) : 0
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 【优化】强制停止Chrome进程
|
|
|
if ($process->isRunning()) {
|
|
if ($process->isRunning()) {
|
|
|
- $process->stop(5, $killSignal);
|
|
|
|
|
|
|
+ Log::warning('ExamPdfExportService: Chrome进程仍在运行,强制停止', [
|
|
|
|
|
+ 'elapsed' => round($elapsed, 2) . 's'
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $process->stop(2, $killSignal);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$process->wait();
|
|
$process->wait();
|
|
|
|
|
|
|
|
- } catch (ProcessTimedOutException|ProcessSignaledException $e) {
|
|
|
|
|
|
|
+ } catch (ProcessTimedOutException $e) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Chrome进程超时', [
|
|
|
|
|
+ 'timeout' => $e->getExceededTimeout(),
|
|
|
|
|
+ 'elapsed' => round(microtime(true) - $startedAt, 2) . 's',
|
|
|
|
|
+ 'html_size' => file_exists($htmlPath) ? filesize($htmlPath) : 0
|
|
|
|
|
+ ]);
|
|
|
if ($process->isRunning()) {
|
|
if ($process->isRunning()) {
|
|
|
- $process->stop(5, $killSignal);
|
|
|
|
|
|
|
+ $process->stop(2, $killSignal);
|
|
|
|
|
+ }
|
|
|
|
|
+ return $this->handleChromeProcessResult($tmpPdf, $userDataDir, $process, $startedAt);
|
|
|
|
|
+ } catch (ProcessSignaledException $e) {
|
|
|
|
|
+ Log::warning('ExamPdfExportService: Chrome进程被信号终止', [
|
|
|
|
|
+ 'signal' => $e->getSignal(),
|
|
|
|
|
+ 'elapsed' => round(microtime(true) - $startedAt, 2) . 's'
|
|
|
|
|
+ ]);
|
|
|
|
|
+ if ($process->isRunning()) {
|
|
|
|
|
+ $process->stop(2, $killSignal);
|
|
|
}
|
|
}
|
|
|
return $this->handleChromeProcessResult($tmpPdf, $userDataDir, $process, $startedAt);
|
|
return $this->handleChromeProcessResult($tmpPdf, $userDataDir, $process, $startedAt);
|
|
|
} catch (\Throwable $e) {
|
|
} catch (\Throwable $e) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Chrome渲染异常', [
|
|
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
|
|
+ 'elapsed' => round(microtime(true) - $startedAt, 2) . 's',
|
|
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
|
|
+ ]);
|
|
|
if ($process->isRunning()) {
|
|
if ($process->isRunning()) {
|
|
|
- $process->stop(5, $killSignal);
|
|
|
|
|
|
|
+ $process->stop(2, $killSignal);
|
|
|
}
|
|
}
|
|
|
return $this->handleChromeProcessResult($tmpPdf, $userDataDir, $process, null);
|
|
return $this->handleChromeProcessResult($tmpPdf, $userDataDir, $process, null);
|
|
|
}
|
|
}
|
|
@@ -1343,29 +1399,76 @@ class ExamPdfExportService
|
|
|
*/
|
|
*/
|
|
|
private function handleChromeProcessResult(string $tmpPdf, string $userDataDir, Process $process, ?float $startedAt): ?string
|
|
private function handleChromeProcessResult(string $tmpPdf, string $userDataDir, Process $process, ?float $startedAt): ?string
|
|
|
{
|
|
{
|
|
|
|
|
+ $elapsed = $startedAt ? round(microtime(true) - $startedAt, 2) : 0;
|
|
|
$pdfExists = file_exists($tmpPdf);
|
|
$pdfExists = file_exists($tmpPdf);
|
|
|
$pdfSize = $pdfExists ? filesize($tmpPdf) : null;
|
|
$pdfSize = $pdfExists ? filesize($tmpPdf) : null;
|
|
|
|
|
|
|
|
if (!$process->isSuccessful()) {
|
|
if (!$process->isSuccessful()) {
|
|
|
if ($pdfExists && $pdfSize > 0) {
|
|
if ($pdfExists && $pdfSize > 0) {
|
|
|
Log::warning('ExamPdfExportService: Chrome进程异常但生成了PDF', [
|
|
Log::warning('ExamPdfExportService: Chrome进程异常但生成了PDF', [
|
|
|
|
|
+ 'elapsed' => $elapsed . 's',
|
|
|
'exit_code' => $process->getExitCode(),
|
|
'exit_code' => $process->getExitCode(),
|
|
|
'tmp_pdf_size' => $pdfSize,
|
|
'tmp_pdf_size' => $pdfSize,
|
|
|
]);
|
|
]);
|
|
|
} else {
|
|
} else {
|
|
|
Log::error('ExamPdfExportService: Chrome渲染失败', [
|
|
Log::error('ExamPdfExportService: Chrome渲染失败', [
|
|
|
|
|
+ 'elapsed' => $elapsed . 's',
|
|
|
'exit_code' => $process->getExitCode(),
|
|
'exit_code' => $process->getExitCode(),
|
|
|
'error' => $process->getErrorOutput(),
|
|
'error' => $process->getErrorOutput(),
|
|
|
|
|
+ 'tmp_pdf_exists' => $pdfExists,
|
|
|
|
|
+ 'tmp_pdf_size' => $pdfSize
|
|
|
]);
|
|
]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Log::info('ExamPdfExportService: Chrome进程正常结束', [
|
|
|
|
|
+ 'elapsed' => $elapsed . 's',
|
|
|
|
|
+ 'exit_code' => $process->getExitCode(),
|
|
|
|
|
+ 'pdf_exists' => $pdfExists,
|
|
|
|
|
+ 'pdf_size' => $pdfSize
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 读取PDF内容
|
|
|
|
|
+ $pdfBinary = null;
|
|
|
|
|
+ if ($pdfExists && $pdfSize > 0) {
|
|
|
|
|
+ $pdfBinary = file_get_contents($tmpPdf);
|
|
|
|
|
+ Log::info('ExamPdfExportService: PDF读取成功', [
|
|
|
|
|
+ 'size' => strlen($pdfBinary),
|
|
|
|
|
+ 'elapsed' => $elapsed . 's'
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Log::error('ExamPdfExportService: PDF文件不存在或为空', [
|
|
|
|
|
+ 'tmp_pdf' => $tmpPdf,
|
|
|
|
|
+ 'exists' => $pdfExists,
|
|
|
|
|
+ 'size' => $pdfSize
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 【优化】确保资源正确释放,使用try-catch包装
|
|
|
|
|
+ try {
|
|
|
|
|
+ if ($pdfExists && file_exists($tmpPdf)) {
|
|
|
@unlink($tmpPdf);
|
|
@unlink($tmpPdf);
|
|
|
|
|
+ Log::debug('ExamPdfExportService: 临时PDF文件已删除', ['path' => $tmpPdf]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
|
+ Log::warning('ExamPdfExportService: 删除临时PDF文件失败', [
|
|
|
|
|
+ 'path' => $tmpPdf,
|
|
|
|
|
+ 'error' => $e->getMessage()
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if ($userDataDir && is_dir($userDataDir)) {
|
|
|
File::deleteDirectory($userDataDir);
|
|
File::deleteDirectory($userDataDir);
|
|
|
- return null;
|
|
|
|
|
|
|
+ Log::debug('ExamPdfExportService: Chrome用户数据目录已删除', ['path' => $userDataDir]);
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
|
+ Log::warning('ExamPdfExportService: 删除Chrome用户数据目录失败', [
|
|
|
|
|
+ 'path' => $userDataDir,
|
|
|
|
|
+ 'error' => $e->getMessage()
|
|
|
|
|
+ ]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $pdfBinary = $pdfExists ? file_get_contents($tmpPdf) : null;
|
|
|
|
|
- @unlink($tmpPdf);
|
|
|
|
|
- File::deleteDirectory($userDataDir);
|
|
|
|
|
return $pdfBinary ?: null;
|
|
return $pdfBinary ?: null;
|
|
|
}
|
|
}
|
|
|
|
|
|