Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 73 additions & 16 deletions lib/exportJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,70 @@ const logger = debug('electronpdf:')
const wargs = require('./args')

// CONSTANTS
/** Used to calculate browser dimensions based on PDF size */
const HTML_DPI = 96
/** Interval for which to check for hung windows, in milliseconds */
const HUNG_WINDOW_CLEANUP_INTERVAL = process.env.ELECTRONPDF_WINDOW_CLEANUP_INTERVAL || 1000 * 30 /* seconds */
/** How long a window can remain open before it is terminated, in milliseconds */
const HUNG_WINDOW_THRESHOLD = process.env.ELECTRONPDF_WINDOW_LIFE_THRESHOLD || 1000 * 60 * 5 /* minutes */
/** Used to determine browser size using a Micron -> Inch -> Pixel conversion */
const MICRONS_INCH_RATIO = 25400
const MAX_EVENT_WAIT = 10000
/** When a ready event option is set, this is the default timeout. It is overridden by the wait option */
const MAX_READY_EVENT_WAIT = 10000
/** The event name for which Electron IPC is done over */
const IPC_MAIN_CHANNEL_RENDER = 'READY_TO_RENDER'
const eventPrefix = 'job.render.'
/** Prepended to events emitted during rendering */
const RENDER_EVENT_PREFIX = 'job.render.'

const DEFAULT_OPTIONS = {
closeWindow: true,
inMemory: false
}

// Window Cache - Keep track of all windows created, and if any get stuck close
// them
const windowCache = {}
/**
* When a job creates a window it invoks this method so any memory leaks
* due to hung windows are prevented. This can happen if an uncaught exception
* occurs and job.destroy() is never invoked.
* @param job
*/
function registerOpenWindow (job) {
const w = job.window
windowCache[w.id] = {job: job, window: w, lastUsed: Date.now()}
}
/**
* Anytime a window is used this function should be invoked to update
* the lastUsed property in the window cache
* @param id
*/
function touchWindow (id) {
windowCache[id].lastUsed = Date.now()
}

function cleanupHungWindows () {
const now = Date.now()
const hungWindows = _.filter(windowCache,
e => now - e.lastUsed > HUNG_WINDOW_THRESHOLD)
logger(`checking hung windows. total windows: ${_.size(windowCache)}`)
_.forEach(hungWindows, e => {
const windowContext = {
id: e.window.id,
lifespan: now - e.lastUsed
}
e.job.emit('window.termination', windowContext)
delete windowCache[e.window.id]
e.job.destroy()
})
}

setInterval(cleanupHungWindows, HUNG_WINDOW_CLEANUP_INTERVAL)

/**
* A job should be created to process a given export opreation for one or more
* resources and a set of output options.
*/
class ExportJob extends EventEmitter {

/**
Expand Down Expand Up @@ -84,20 +137,20 @@ class ExportJob extends EventEmitter {
/**
* Render markdown or html to pdf
*/
render (window) {
this.emit(`${eventPrefix}start`)
render () {
this.emit(`${RENDER_EVENT_PREFIX}start`)

const win = this._launchBrowserWindow()
this.window = win
registerOpenWindow(this)

// TODO: Check for different domains, this is meant to support only a single origin
const firstUrl = this.input[0]
this._setSessionCookies(this.args.cookies, firstUrl, win.webContents.session.cookies)

const windowEvents = []
// The same listeners can be used for each resource
this._passThroughEvents(win, eventPrefix)

this._passThroughEvents(win, RENDER_EVENT_PREFIX)
this.input.forEach((uriPath, i) => {
windowEvents.push((pageDone) => {
this._initializeWindowForResource()
Expand All @@ -114,7 +167,7 @@ class ExportJob extends EventEmitter {
async.series(windowEvents, (err, results) => {
if (this.options.closeWindow) {
win.close()
this.emit(`${eventPrefix}window.close`)
this.emit(`${RENDER_EVENT_PREFIX}window.close`)
}
/**
* PDF Generation Event - fires when all PDFs have been persisted to disk
Expand All @@ -123,7 +176,7 @@ class ExportJob extends EventEmitter {
* @property {String} results - array of generated pdf file locations
* @property {Object} error - If an error occurred, null otherwise
*/
this.emit(`${eventPrefix}complete`, {results: results, error: err})
this.emit(`${RENDER_EVENT_PREFIX}complete`, {results: results, error: err})
this.emit('job-complete', {results: results, error: err}) // Deprecated
})
}
Expand Down Expand Up @@ -237,6 +290,7 @@ class ExportJob extends EventEmitter {
* @private
*/
_initializeWindowForResource () {
touchWindow(this.window.id)
// Reset the generated flag for each input URL because this same job/window
// can be reused in this scenario
this.generated = false
Expand Down Expand Up @@ -299,7 +353,8 @@ class ExportJob extends EventEmitter {
}

/**
* Inpects this.args and sets the window size based on the pageSize and orientation
* Inpects this.args and sets the window size based on the pageSize and
* orientation
* @private
*/
_setWindowDimensions () {
Expand Down Expand Up @@ -401,7 +456,7 @@ class ExportJob extends EventEmitter {
if (this.args.disableCache) {
loadOpts.extraHeaders += 'pragma: no-cache\n'
}
this.emit(`${eventPrefix}loadurl`, { url: url })
this.emit(`${RENDER_EVENT_PREFIX}loadurl`, { url: url })
window.loadURL(wargs.urlWithArgs(url, {}), loadOpts)
}

Expand Down Expand Up @@ -454,7 +509,7 @@ class ExportJob extends EventEmitter {

// Don't let a ready event hang, set a max timeout interval
const f = this._cancelReadyEvent.bind(this, eventName, ipcListener, generateFunction)
const maxWait = this.args.outputWait > 0 ? this.args.outputWait : MAX_EVENT_WAIT
const maxWait = this.args.outputWait > 0 ? this.args.outputWait : MAX_READY_EVENT_WAIT
const timeout = setTimeout(f, maxWait)

// clear the timeout as soon as we get the ready event from the browser
Expand All @@ -464,11 +519,13 @@ class ExportJob extends EventEmitter {
}

/**
* Invoked when a ready event has not been received before the max timeout is reached
* Invoked when a ready event has not been received before the max timeout is
* reached
* @param eventName The eventName provided by the client
* @param ipcListener The ipcMain listener waiting for the IPC_MAIN_CHANNEL_RENDER
* event from the renderer process
* @param generateFunction A callback function to invoke to capture the window
* @param ipcListener The ipcMain listener waiting for the
* IPC_MAIN_CHANNEL_RENDER event from the renderer process
* @param generateFunction A callback function to invoke to capture the
* window
* @private
*/
_cancelReadyEvent (eventName, ipcListener, generateFunction) {
Expand Down Expand Up @@ -531,7 +588,7 @@ class ExportJob extends EventEmitter {
*/
this.emit('window.observer.timeout', {})
generateFunction()
}, MAX_EVENT_WAIT)
}, MAX_READY_EVENT_WAIT)

this.readyEventObserver(customEventDetail).then(() => {
/**
Expand Down