@@ -19,17 +19,70 @@ const logger = debug('electronpdf:')
1919const wargs = require ( './args' )
2020
2121// CONSTANTS
22+ /** Used to calculate browser dimensions based on PDF size */
2223const 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 */
2329const 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 */
2533const 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
2837const 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+ */
3386class 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