Warm tip: This article is reproduced from serverfault.com, please click

PM2 & Puppeteer Watch Restarting

发布于 2020-11-30 01:27:03

I have a puppeteer script which I am using to produce an export from a reporting tool that I use (called pivot.js):

const fs = require('fs');
const path = require('path');
const events = require('events');
const puppeteer = require('puppeteer');

let eventEmitter = new events.EventEmitter();

const directoryPath = "./storage/"; /* A path to the storage of exported files */

((directoryPath) => {
  fs.mkdir(path.resolve(path.resolve(),
    directoryPath.replace(/^\.*\/|\/?[^\/]+\.[a-z]+|\/$/g, '')), { recursive: true }, error => {
      if (error) console.error(error);
    });
})(directoryPath); /* Creating a storage folder for exported files (if such a folder doesn't exist yet) */

(async () => {

  eventEmitter.once('reportcomplete', () => {

    /*
      All changes should be made within this function.
 
      Available methods:
      - setReport (https://www.flexmonster.com/api/setreport/)
      - exportTo (https://www.flexmonster.com/api/exportto/)
 
      The exportTo method takes two parameters: type and params.
      Callback function will be ignored.
      Possible destination types:
      - plain (the file will be saved by the path defined as a value of the "directoryPath" variable)
      - server (the file will be exported to the server)
 
      Available events (use "eventEmitter" to manage events):
      - ready (https://www.flexmonster.com/api/ready/)
      - reportcomplete (https://www.flexmonster.com/api/reportcomplete/)
      - exportcomplete (https://www.flexmonster.com/api/exportcomplete/)
 
      Additional methods and events can be added using the template.
    */

    eventEmitter.once('reportcomplete', () => { /* Exporting when the report is ready */
      exportTo("csv");
      exportTo("html");
      exportTo("pdf");
      exportTo("image");
      exportTo("excel");
    });

    let exportCount = 0;
    eventEmitter.on('exportcomplete', () => {
      exportCount++;
      if (exportCount == 5) browser.close(); /* Closing the browser when all the exports are complete */
    });

    setReport({
      dataSource: {
        filename: 'https://cdn.flexmonster.com/data/data.json'
      }
    });

  });

  const browser = await puppeteer.launch(); /* Launching the headless browser */
  const page = await browser.newPage(); /* Creating a new page */

  /* A function to set a report for the component dynamically */
  function setReport(report) {
    page.evaluate(report => {
      flexmonster.setReport(report);
    }, report)
  }

  /* This code is responsible for the export itself. It supports five export formats: 
     .html, .xlsx, .pdf, .csv, and .png. */
  function exportTo(type, params) {
    page.evaluate((type, params) => {
      type = type.toLowerCase();
      if (params) {
        if (params.destinationType != "plain" && params.destinationType != "server")
          params.destinationType = "plain";
      }
      else params = { destinationType: "plain" };
      if (!params.filename) params.filename = "pivot";
      flexmonster.exportTo(type, params, (result) => {
        switch (type) {
          case "pdf":
            result.data = result.data.output();
            break;
          case "excel":
            result.data = Array.from(result.data);
            break;
          case "image":
            result.data = result.data.toDataURL();
            break;
        }
        exportHandler(result);
      });
    }, type, params);
  }

  await page.exposeFunction('exportHandler', (result) => {
    switch (result.type) {
      case "excel":
        result.data = Buffer.from(result.data);
        result.type = "xlsx";
        break;
      case "image":
        result.data = Buffer.from(result.data.replace(/^data:image\/\w+;base64,/, ""), 'base64');
        result.type = "png";
        break;
    }
    fs.writeFile(`${directoryPath}${result.filename}.${result.type}`, result.data, 'utf8', error => {
      if (error) console.log(error);
    });
  });


  /* This code adds functions to emit ready, reportcomplete, and exportcomplete events for the browser 
     when called. This approach allows us to handle the component's events in the browser's scope. */
  await page.exposeFunction('onReady', () => {
    eventEmitter.emit('ready')
  });
  await page.exposeFunction('onReportComplete', () => {
    eventEmitter.emit('reportcomplete')
  });
  await page.exposeFunction('onExportComplete', () => {
    eventEmitter.emit('exportcomplete')
  });

  /*  Reading the file with the component and setting it as the browser page's contents */
  await page.setContent(fs.readFileSync('index.html', 'utf8'));

  /* This code runs in the page's scope, subscribing the browser window to the component's ready, 
     reportcomplete, and exportcomplete events */
  await page.evaluate(() => {
    window.addEventListener('ready', () => window.onReady());
    window.addEventListener('reportcomplete', () => window.onReportComplete());
    window.addEventListener('exportcomplete', () => window.onExportComplete());
  });

})();

I am then using PM2 to watch the file, allowing me to swap out the code used to produce different reports, using:

pm2 start pivot.js --watch

The issue I have is that whenever I delete the contents of the storage folder (which the script writes into), a new export appears straight away. Almost as if the script is continuously being called, or PM2 is being restarted.

Both of the logs for PM2 are completely blank. But after running:

pm2 show 0

I receive the following:

│ status            │ stopping                                            │
│ name              │ pivot                                               │
│ namespace         │ default                                             │
│ version           │ 1.0.0                                               │
│ restarts          │ 1957                                                │
│ uptime            │ 0                                                   │
│ script path       │ C:\Users\admin\Documents\Windows Puppeteer\pivot.js │
│ script args       │ N/A                                                 │
│ error log path    │ C:\Users\admin\.pm2\logs\pivot-error.log            │
│ out log path      │ C:\Users\admin\.pm2\logs\pivot-out.log              │
│ pid path          │ C:\Users\admin\.pm2\pids\pivot-0.pid                │
│ interpreter       │ node                                                │
│ interpreter args  │ N/A                                                 │
│ script id         │ 0                                                   │
│ exec cwd          │ C:\Users\admin\Documents\Windows Puppeteer          │
│ exec mode         │ fork_mode                                           │
│ node.js version   │ 14.15.1                                             │
│ node env          │ N/A                                                 │
│ watch & reload    │ ✔                                                   │
│ unstable restarts │ 0                                                   │
│ created at        │ 2020-11-30T01:24:27.461Z                            │

I hope you can help.

Questioner
Scott
Viewed
0
Scott 2020-12-03 09:49:29

The issue is that the puppeteer script (named pivot.js) dumps the returning file in a folder called "storage". Storage is within the same directory as pivot.js, meaning that whilst PM2 monitors that directory, it is creating an infinite loop. The solution is to use the ignore watch option.

Creating a ecosystem file as such:

module.exports = {
  apps : [{
    script: 'pivot.js',
    watch: '.',
    ignore_watch : ["node_modules", "storage"]
  }],
  ...
};

Or using:

pm2 start pivot.js --watch --ignore-watch="storage"

In my examples above will resolve the problem.