Skip to content

Commit 88467e8

Browse files
hung window cleanup, improved code docs. Fixes #163 (#165)
* hung window cleanup, improved code docs. Fixes #163 * document units for timed constants #163
1 parent a7ba6d3 commit 88467e8

File tree

1 file changed

+73
-16
lines changed

1 file changed

+73
-16
lines changed

lib/exportJob.js

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,70 @@ const logger = debug('electronpdf:')
1919
const wargs = require('./args')
2020

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

2837
const DEFAULT_OPTIONS = {
2938
closeWindow: true,
3039
inMemory: false
3140
}
3241

42+
// Window Cache - Keep track of all windows created, and if any get stuck close
43+
// them
44+
const windowCache = {}
45+
/**
46+
* When a job creates a window it invoks this method so any memory leaks
47+
* due to hung windows are prevented. This can happen if an uncaught exception
48+
* occurs and job.destroy() is never invoked.
49+
* @param job
50+
*/
51+
function registerOpenWindow (job) {
52+
const w = job.window
53+
windowCache[w.id] = {job: job, window: w, lastUsed: Date.now()}
54+
}
55+
/**
56+
* Anytime a window is used this function should be invoked to update
57+
* the lastUsed property in the window cache
58+
* @param id
59+
*/
60+
function touchWindow (id) {
61+
windowCache[id].lastUsed = Date.now()
62+
}
63+
64+
function cleanupHungWindows () {
65+
const now = Date.now()
66+
const hungWindows = _.filter(windowCache,
67+
e => now - e.lastUsed > HUNG_WINDOW_THRESHOLD)
68+
logger(`checking hung windows. total windows: ${_.size(windowCache)}`)
69+
_.forEach(hungWindows, e => {
70+
const windowContext = {
71+
id: e.window.id,
72+
lifespan: now - e.lastUsed
73+
}
74+
e.job.emit('window.termination', windowContext)
75+
delete windowCache[e.window.id]
76+
e.job.destroy()
77+
})
78+
}
79+
80+
setInterval(cleanupHungWindows, HUNG_WINDOW_CLEANUP_INTERVAL)
81+
82+
/**
83+
* A job should be created to process a given export opreation for one or more
84+
* resources and a set of output options.
85+
*/
3386
class ExportJob extends EventEmitter {
3487

3588
/**
@@ -84,20 +137,20 @@ class ExportJob extends EventEmitter {
84137
/**
85138
* Render markdown or html to pdf
86139
*/
87-
render (window) {
88-
this.emit(`${eventPrefix}start`)
140+
render () {
141+
this.emit(`${RENDER_EVENT_PREFIX}start`)
89142

90143
const win = this._launchBrowserWindow()
91144
this.window = win
145+
registerOpenWindow(this)
92146

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

97151
const windowEvents = []
98152
// The same listeners can be used for each resource
99-
this._passThroughEvents(win, eventPrefix)
100-
153+
this._passThroughEvents(win, RENDER_EVENT_PREFIX)
101154
this.input.forEach((uriPath, i) => {
102155
windowEvents.push((pageDone) => {
103156
this._initializeWindowForResource()
@@ -114,7 +167,7 @@ class ExportJob extends EventEmitter {
114167
async.series(windowEvents, (err, results) => {
115168
if (this.options.closeWindow) {
116169
win.close()
117-
this.emit(`${eventPrefix}window.close`)
170+
this.emit(`${RENDER_EVENT_PREFIX}window.close`)
118171
}
119172
/**
120173
* PDF Generation Event - fires when all PDFs have been persisted to disk
@@ -123,7 +176,7 @@ class ExportJob extends EventEmitter {
123176
* @property {String} results - array of generated pdf file locations
124177
* @property {Object} error - If an error occurred, null otherwise
125178
*/
126-
this.emit(`${eventPrefix}complete`, {results: results, error: err})
179+
this.emit(`${RENDER_EVENT_PREFIX}complete`, {results: results, error: err})
127180
this.emit('job-complete', {results: results, error: err}) // Deprecated
128181
})
129182
}
@@ -237,6 +290,7 @@ class ExportJob extends EventEmitter {
237290
* @private
238291
*/
239292
_initializeWindowForResource () {
293+
touchWindow(this.window.id)
240294
// Reset the generated flag for each input URL because this same job/window
241295
// can be reused in this scenario
242296
this.generated = false
@@ -299,7 +353,8 @@ class ExportJob extends EventEmitter {
299353
}
300354

301355
/**
302-
* Inpects this.args and sets the window size based on the pageSize and orientation
356+
* Inpects this.args and sets the window size based on the pageSize and
357+
* orientation
303358
* @private
304359
*/
305360
_setWindowDimensions () {
@@ -401,7 +456,7 @@ class ExportJob extends EventEmitter {
401456
if (this.args.disableCache) {
402457
loadOpts.extraHeaders += 'pragma: no-cache\n'
403458
}
404-
this.emit(`${eventPrefix}loadurl`, { url: url })
459+
this.emit(`${RENDER_EVENT_PREFIX}loadurl`, { url: url })
405460
window.loadURL(wargs.urlWithArgs(url, {}), loadOpts)
406461
}
407462

@@ -454,7 +509,7 @@ class ExportJob extends EventEmitter {
454509

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

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

466521
/**
467-
* Invoked when a ready event has not been received before the max timeout is reached
522+
* Invoked when a ready event has not been received before the max timeout is
523+
* reached
468524
* @param eventName The eventName provided by the client
469-
* @param ipcListener The ipcMain listener waiting for the IPC_MAIN_CHANNEL_RENDER
470-
* event from the renderer process
471-
* @param generateFunction A callback function to invoke to capture the window
525+
* @param ipcListener The ipcMain listener waiting for the
526+
* IPC_MAIN_CHANNEL_RENDER event from the renderer process
527+
* @param generateFunction A callback function to invoke to capture the
528+
* window
472529
* @private
473530
*/
474531
_cancelReadyEvent (eventName, ipcListener, generateFunction) {
@@ -531,7 +588,7 @@ class ExportJob extends EventEmitter {
531588
*/
532589
this.emit('window.observer.timeout', {})
533590
generateFunction()
534-
}, MAX_EVENT_WAIT)
591+
}, MAX_READY_EVENT_WAIT)
535592

536593
this.readyEventObserver(customEventDetail).then(() => {
537594
/**

0 commit comments

Comments
 (0)