From c488c08501107c9096af8c0c8deb09588cd121b3 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 21 Jul 2015 19:02:08 -0700 Subject: [PATCH] prevent memory leaks when using multiple screens. see #157. --- README.md | 4 ++ lib/program.js | 68 ++++++++++++++++++++------- lib/widgets/screen.js | 105 +++++++++++++++++++++++++++++------------- 3 files changed, 128 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index ce550bd..412c7d4 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,10 @@ The screen on which every other node renders. - __screenshot([xi, xl, yi, yl])__ - Take an SGR screenshot of the screen within the region. Returns a string containing only characters and SGR codes. Can be displayed by simply echoing it in a terminal. +- __destroy()__ - destroy the screen object and remove it from the global list. + only useful if using multiple screens. +- __program.destroy()__ - destroy the program object and remove it from the + global list. only useful if using multiple programs. #### Element (from Node) diff --git a/lib/program.js b/lib/program.js index d25da4e..db3ae3c 100644 --- a/lib/program.js +++ b/lib/program.js @@ -31,6 +31,8 @@ function Program(options) { return new Program(options); } + Program.bind(this); + EventEmitter.call(this); if (!options || options.__proto__ !== Object.prototype) { @@ -104,19 +106,6 @@ function Program(options) { this._buf = ''; this._flush = this.flush.bind(this); - unshiftEvent(process, 'exit', function() { - // Ensure the buffer is flushed (it should - // always be at this point, but who knows). - self.flush(); - // Ensure _exiting is set (could technically - // use process._exiting). - self._exiting = true; - }); - - if (!Program.global) { - Program.global = this; - } - if (options.tput !== false) { this.setupTput(); } @@ -124,8 +113,41 @@ function Program(options) { this.listen(); } +Program.global = null; + +Program.total = 0; + +Program.list = []; + +Program.bind = function(program) { + if (!Program.global) { + Program.global = program; + } + + if (!~Program.list.indexOf(program)) { + Program.list.push(program); + Program.total++; + } + + if (Program._bound) return; + Program._bound = true; + + unshiftEvent(process, 'exit', function() { + Program.list.forEach(function(program) { + // Ensure the buffer is flushed (it should + // always be at this point, but who knows). + program.flush(); + // Ensure _exiting is set (could technically + // use process._exiting). + program._exiting = true; + }); + }); +}; + Program.prototype.__proto__ = EventEmitter.prototype; +Program.prototype.type = 'program'; + Program.prototype.log = function() { return this._log('LOG', util.format.apply(util, arguments)); }; @@ -272,9 +294,8 @@ Program.prototype.listen = function() { var keys = require('./keys') , self = this; - if (!this.output.isTTY) { - // TODO: Add an ncurses-like warning. - } + if (this.input._blessedListened) return; + this.input._blessedListened = true; // unshiftEvent(process, 'exit', function() { // if (self._originalTitle) { @@ -344,6 +365,9 @@ Program.prototype.listen = function() { } }); + if (this.output._blessedListened) return; + this.output._blessedListened = true; + // Output function resize() { self.cols = self.output.columns; @@ -366,6 +390,18 @@ Program.prototype.listen = function() { }); }; +Program.prototype.destroy = function() { + var index = Program.list.indexOf(this); + if (~index) { + Program.list.splice(index, 1); + Program.total--; + if (Program.total === 0) { + Program.global = null; + } + this.emit('destroy'); + } +}; + Program.prototype.key = function(key, listener) { if (typeof key === 'string') key = key.split(/\s*,\s*/); key.forEach(function(key) { diff --git a/lib/widgets/screen.js b/lib/widgets/screen.js index acb25ea..b91edda 100644 --- a/lib/widgets/screen.js +++ b/lib/widgets/screen.js @@ -35,6 +35,8 @@ function Screen(options) { return new Screen(options); } + Screen.bind(this); + options = options || {}; if (options.rsety && options.listen) { options = { program: options }; @@ -64,10 +66,6 @@ function Screen(options) { this.tput = this.program.tput; - if (!Screen.global) { - Screen.global = this; - } - Node.call(this, options); this.autoPadding = options.autoPadding !== false; @@ -175,35 +173,6 @@ function Screen(options) { this.setMaxListeners(Infinity); - Screen.total++; - - process.on('uncaughtException', function(err) { - if (process.listeners('uncaughtException').length > Screen.total) { - return; - } - self.leave(); - err = err || new Error('Uncaught Exception.'); - console.error(err.stack ? err.stack + '' : err + ''); - nextTick(function() { - process.exit(1); - }); - }); - - ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(function(signal) { - process.on(signal, function() { - if (process.listeners(signal).length > Screen.total) { - return; - } - nextTick(function() { - process.exit(0); - }); - }); - }); - - process.on('exit', function() { - self.leave(); - }); - this.enter(); this.postEnter(); @@ -213,6 +182,59 @@ Screen.global = null; Screen.total = 0; +Screen.list = []; + +Screen.signals = true; + +Screen.bind = function(screen) { + if (!Screen.global) { + Screen.global = screen; + } + + if (!~Screen.list.indexOf(screen)) { + Screen.list.push(screen); + Screen.total++; + } + + if (Screen._bound) return; + Screen._bound = true; + + process.on('uncaughtException', function(err) { + if (process.listeners('uncaughtException').length > Screen.total) { + return; + } + Screen.list.slice().forEach(function(screen) { + screen.destroy(); + }); + err = err || new Error('Uncaught Exception.'); + console.error(err.stack ? err.stack + '' : err + ''); + nextTick(function() { + process.exit(1); + }); + }); + + // XXX Multiple signal handlers and removal of signal + // handlers does not work, so we have this option instead. + if (Screen.signals) { + ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(function(signal) { + process.on(signal, function() { + if (process.listeners(signal).length > Screen.total) { + return; + } + nextTick(function() { + process.exit(0); + }); + }); + }); + } + + process.on('exit', function() { + Screen.list.slice().forEach(function(screen) { + screen.destroy(); + }); + }); +}; + Screen.prototype.__proto__ = Node.prototype; Screen.prototype.type = 'screen'; @@ -284,6 +306,7 @@ Screen.prototype.postEnter = function() { var self = this; if (this.options.debug) { this.debugLog = new Log({ + screen: this, parent: this, hidden: true, draggable: true, @@ -326,6 +349,22 @@ Screen.prototype.postEnter = function() { } }; +Screen.prototype.destroy = function() { + this.leave(); + + var index = Screen.list.indexOf(this); + if (~index) { + Screen.list.splice(index, 1); + Screen.total--; + if (Screen.total === 0) { + Screen.global = null; + } + this.emit('destroy'); + } + + this.program.destroy(); +}; + Screen.prototype.log = function() { if (this.debugLog) { this.debugLog.log.apply(this.debugLog, arguments);