Skip to content

Commit

Permalink
hung window cleanup, improved code docs. Fixes #163 (#165)
Browse files Browse the repository at this point in the history
* hung window cleanup, improved code docs. Fixes #163

* document units for timed constants #163
  • Loading branch information
codecounselor committed Feb 9, 2017
1 parent a7ba6d3 commit 88467e8
Showing 1 changed file with 73 additions and 16 deletions.
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

0 comments on commit 88467e8

Please sign in to comment.