diff --git a/src/services/printer.processor.ts b/src/services/printer.processor.ts index c235893..e02bc83 100644 --- a/src/services/printer.processor.ts +++ b/src/services/printer.processor.ts @@ -1,26 +1,113 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger, OnApplicationShutdown } from '@nestjs/common'; import { Job } from 'bullmq'; +import { performance } from 'node:perf_hooks'; import * as puppeteer from 'puppeteer'; import * as pdfPrinter from 'pdf-to-printer'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; +import { PrintHtmlDto } from '../dto/print-html.dto'; @Processor('printer') -export class PrinterProcessor extends WorkerHost { - - async process(job: Job): Promise { +export class PrinterProcessor + extends WorkerHost + implements OnApplicationShutdown +{ + private readonly logger = new Logger(PrinterProcessor.name); + private browser: puppeteer.Browser | undefined; + private browserLaunching: Promise | undefined; + + private async getBrowser(): Promise { + if (this.browser?.isConnected()) return this.browser; + if (this.browserLaunching) return this.browserLaunching; + + this.browserLaunching = puppeteer + .launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }) + .then((browser) => { + this.browser = browser; + return browser; + }) + .finally(() => { + this.browserLaunching = undefined; + }); + + return this.browserLaunching; + } + + async onApplicationShutdown(): Promise { + try { + await this.browser?.close(); + } catch (err) { + this.logger.warn(`Failed to close Puppeteer browser: ${String(err)}`); + } finally { + this.browser = undefined; + } + } + + private logJob( + level: 'log' | 'warn' | 'error' | 'debug', + payload: Record, + error?: unknown, + ): void { + const message = JSON.stringify(payload); + if (level === 'error') { + const stack = error instanceof Error ? error.stack : undefined; + this.logger.error(message, stack); + return; + } + + if (level === 'warn') { + this.logger.warn(message); + return; + } + + if (level === 'debug') { + this.logger.debug(message); + return; + } + + this.logger.log(message); + } + + async process(job: Job): Promise { const data = job.data; - const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`); - let browser; + const printerName = data?.printerName; + const jobCtx = { + jobId: job.id, + jobName: job.name, + printerName, + }; + + const tempFilePath = path.join( + os.tmpdir(), + `print-${job.id}-${Date.now()}.pdf`, + ); + let page: puppeteer.Page | undefined; + let phase: 'render' | 'pdf' | 'spool' | 'unknown' = 'unknown'; + + const durationsMs: { + render?: number; + pdf?: number; + spool?: number; + total?: number; + } = {}; + + const totalStart = performance.now(); + + this.logJob('log', { + event: 'print_job_start', + ...jobCtx, + width: data?.width, + height: data?.height, + }); try { - browser = await puppeteer.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'] - }); - - const page = await browser.newPage(); + const browser = await this.getBrowser(); + page = await browser.newPage(); const width = data.width || '80mm'; const height = data.height; @@ -40,30 +127,97 @@ export class PrinterProcessor extends WorkerHost { `; + phase = 'render'; + const renderStart = performance.now(); await page.setContent(fullHtml, { waitUntil: 'networkidle0' }); - - const pdfOptions: any = { - path: tempFilePath, + durationsMs.render = performance.now() - renderStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.render, + ...jobCtx, + }); + + const pdfOptions: any = { + path: tempFilePath, printBackground: true, width: width, - margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' } + margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }, }; if (height) pdfOptions.height = height; + phase = 'pdf'; + const pdfStart = performance.now(); await page.pdf(pdfOptions); - await browser.close(); + durationsMs.pdf = performance.now() - pdfStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.pdf, + ...jobCtx, + }); - await pdfPrinter.print(tempFilePath, { + await page.close(); + page = undefined; + + phase = 'spool'; + const spoolStart = performance.now(); + await pdfPrinter.print(tempFilePath, { printer: data.printerName, - silent: true + silent: true, + }); + durationsMs.spool = performance.now() - spoolStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.spool, + ...jobCtx, + }); + + durationsMs.total = performance.now() - totalStart; + this.logJob('log', { + event: 'print_job_success', + ...jobCtx, + durationsMs, }); return { status: 'success', jobId: job.id }; - } catch (error) { - if (browser) await browser.close(); - console.error(`Erro no Job ${job.id}:`, error); + if (page) { + try { + await page.close(); + } catch { + // ignore + } + } + + const message = error instanceof Error ? error.message : String(error); + durationsMs.total = performance.now() - totalStart; + this.logJob( + 'error', + { + event: 'print_job_error', + ...jobCtx, + phase, + durationsMs, + errorMessage: message, + errorType: error instanceof Error ? error.name : typeof error, + }, + error, + ); + + // If the shared browser crashed/closed, reset it so next job can relaunch. + if (/Target closed|Browser closed|Protocol error/i.test(message)) { + try { + await this.browser?.close(); + } catch { + // ignore + } finally { + this.browser = undefined; + } + } + throw error; } finally { if (fs.existsSync(tempFilePath)) { @@ -71,4 +225,4 @@ export class PrinterProcessor extends WorkerHost { } } } -} \ No newline at end of file +}