Permalink
Browse files

termio.py: Changed the logic that finalizes session logs in Multiplex…

…POSIXIOLoop.terminate() from using a double os.fork() to using a multiprocessing.Process(). It is a lot faster and memory efficient this way. It also lets Gate One restart immediately after being shut down (no more waiting a few seconds for the port to become free!).

termio.py:  For whatever reason IOLoop.set_blocking_signal_threshold() wasn't working to kick off the _blocked_io_handler() so I've gone ahead and wrapped self.term.write() in utils.timeout_func() to ensure that if it takes too long to process the incoming stream it will call _blocked_io_handler().  Also, this change has a nice bonus:  Other terminals/users will continue to operate (albeit slowly) when such a situation occurs.  Using the old logic they would've had to wait 5 seconds for Gate One to start responding again.
termio.py:  _blocked_io_handler() will now call terminate() if sending a Ctrl-c fails to put an end to an out-of-control process.
terminal.py:  Fixed a bug where Terminal() could get stuck thinking it was capturing an image eternally.  The behavior now will be like this:  If an image hasn't completed capture within one second the buffer will be discarded and a message will be displayed indicating as such.
terminal.py:  Renamed terminal_reset() to just reset() since terminal.Terminal.terminal_reset seemed just a *bit* redundant =)
Logging Plugin:  Updated retrieve_log_playback() to use the same logic as retrieve_log_flat() for tracking child processes.  Also updated the logic that removes any existing IOLoop handlers to prevent a transient exception from showing up in the logs when the level is set to "debug".
utils.py:  Modified timeout_func() so that if the *default* argument is a function that function will be called in the event of a timeout (e.g. to make it work more like termio's expect() *errorback* argument--consistency).
gateone.js:  When the rate limiter is engaged a message will be displayed to the user indicating as such along with a message that if a Ctrl-c doesn't fix the out-of-control process it will be terminated.
Playback Plugin:  Made some more tweaks to save memory.  After a while Chrome still starts consuming memory like crazy though--for no apparent reason.  Having it run in the background seems to double the likelihood this will happen.
Playback Plugin:  Fixed the bug where shift+scroll wasn't working to go back/forward in the playback buffer.
NEW TEST:  Added floot_output.sh to the tests directory.  It disables the use of Ctrl-c and repeats, "Kill Me!" as fast as possible.  It is meant to test Gate One's ability to prevent out-of-control processes from ruining everything.
  • Loading branch information...
1 parent 51d3d2b commit 84df9cc7ee25a3e9180642c4f48b6e44d6c66fe0 @liftoff committed Feb 19, 2012
View
@@ -1616,6 +1616,8 @@ def debug_terminal(self, term):
screen = SESSIONS[self.session][term]['multiplex'].term.screen
renditions = SESSIONS[self.session][term]['multiplex'].term.renditions
for i, line in enumerate(screen):
+ # This gets rid of images:
+ line = [a for a in line if len(a) == 1]
print("%s:%s" % (i, "".join(line)))
print(renditions[i])
# Also check if there's anything that's uncollectable
@@ -95,7 +95,9 @@ def enumerate_logs(limit=None, tws=None):
if user not in PROCS:
PROCS[user] = {}
else: # Cancel anything that's already running
- io_loop.remove_handler(PROCS[user]['queue']._reader.fileno())
+ fd = PROCS[user]['queue']._reader.fileno()
+ if fd in io_loop._handlers:
+ io_loop.remove_handler(fd)
if PROCS[user]['process']:
try:
PROCS[user]['process'].terminate()
@@ -194,7 +196,9 @@ def retrieve_log_flat(settings, tws=None):
if user not in PROCS:
PROCS[user] = {}
else: # Cancel anything that's already running
- io_loop.remove_handler(PROCS[user]['queue']._reader.fileno())
+ fd = PROCS[user]['queue']._reader.fileno()
+ if fd in io_loop._handlers:
+ io_loop.remove_handler(fd)
if PROCS[user]['process']:
try:
PROCS[user]['process'].terminate()
@@ -275,10 +279,24 @@ def retrieve_log_playback(settings, tws=None):
settings['users_dir'] = os.path.join(tws.settings['user_dir'], user)
settings['gateone_dir'] = tws.settings['gateone_dir']
settings['url_prefix'] = tws.settings['url_prefix']
- q = Queue()
- global PROC
- PROC = Process(target=_retrieve_log_playback, args=(q, settings))
io_loop = tornado.ioloop.IOLoop.instance()
+ global PROCS
+ if user not in PROCS:
+ PROCS[user] = {}
+ else: # Cancel anything that's already running
+ fd = PROCS[user]['queue']._reader.fileno()
+ if fd in io_loop._handlers:
+ io_loop.remove_handler(fd)
+ if PROCS[user]['process']:
+ try:
+ PROCS[user]['process'].terminate()
+ except OSError:
+ # process was already terminated... Nothing to do
+ pass
+ PROCS[user]['queue'] = q = Queue()
+ PROCS[user]['queue'] = q = Queue()
+ PROCS[user]['process'] = Process(
+ target=_retrieve_log_playback, args=(q, settings))
def send_message(fd, event):
"""
Sends the log enumeration result to the client. Necessary because
@@ -291,8 +309,7 @@ def send_message(fd, event):
# This is kind of neat: multiprocessing.Queue() instances have an
# underlying fd that you can access via the _reader:
io_loop.add_handler(q._reader.fileno(), send_message, io_loop.READ)
- PROC.start()
- return
+ PROCS[user]['process'].start()
def _retrieve_log_playback(queue, settings):
"""
@@ -434,6 +451,7 @@ def save_log_playback(settings, tws=None):
q = Queue()
global PROC
PROC = Process(target=_save_log_playback, args=(q, settings))
+ PROC.daemon = True # We don't care if this gets terminated mid-process.
io_loop = tornado.ioloop.IOLoop.instance()
def send_message(fd, event):
"""
@@ -67,36 +67,35 @@ GateOne.Base.update(GateOne.Playback, {
pushPlaybackFrame: function(termNum) {
// Adds the current screen in *term* to GateOne.terminals[term]['playbackFrames']
var prefix = GateOne.prefs.prefix,
- progressBarElement = null,
term = termNum,
playbackFrames = null,
- frame = {'screen': GateOne.terminals[term]['screen'].slice(0), 'time': new Date()};
- if (!progressBarElement) {
- progressBarElement = GateOne.Utils.getNode('#'+prefix+'progressBar');
+ frame = {'screen': GateOne.terminals[term]['screen'], 'time': new Date()};
+ if (!GateOne.Playback.progressBarElement) {
+ GateOne.Playback.progressBarElement = GateOne.Utils.getNode('#'+prefix+'progressBar');
}
if (!GateOne.terminals[term]['playbackFrames']) {
GateOne.terminals[term]['playbackFrames'] = [];
}
- playbackFrames = GateOne.terminals[term]['playbackFrames'].slice(0);
+ playbackFrames = GateOne.terminals[term]['playbackFrames'];
// Add the new playback frame to the terminal object
playbackFrames.push(frame);
frame = null; // Clean up
- if (GateOne.terminals[term]['playbackFrames'].length > GateOne.prefs.playbackFrames) {
+ if (playbackFrames.length > GateOne.prefs.playbackFrames) {
// Reduce it to fit within the user's configured max
// GateOne.terminals[term]['playbackFrames'].shift(); // NOTE: This won't work if the user reduced their playbackFrames preference by more than 1
playbackFrames.reverse(); // Have to reverse it before we truncate
playbackFrames.length = GateOne.prefs.playbackFrames; // Love that length is assignable!
playbackFrames.reverse(); // Put it back in the right order
}
- GateOne.terminals[term]['playbackFrames'] = null;
- GateOne.terminals[term]['playbackFrames'] = playbackFrames;
+// GateOne.terminals[term]['playbackFrames'] = null;
+// GateOne.terminals[term]['playbackFrames'] = playbackFrames;
// Fix the progress bar if it is in a non-default state and stop playback
- if (progressBarElement) {
- if (progressBarElement.style.width != '0%') {
+ if (GateOne.Playback.progressBarElement) {
+ if (GateOne.Playback.progressBarElement.style.width != '0%') {
clearInterval(GateOne.Playback.frameUpdater);
GateOne.Playback.frameUpdater = null;
GateOne.Playback.milliseconds = 0; // Reset this in case the user was in the middle of playing something back when the screen updated
- progressBarElement.style.width = '0%';
+ GateOne.Playback.progressBarElement.style.width = '0%';
// Also make sure the pastearea is put back if missing
GateOne.Utils.showElement('#'+prefix+'pastearea');
}
@@ -1069,6 +1069,10 @@ GateOne.Base.update(GateOne.Utils, {
}
var digest = Crypto.MD5(seed1*seed2*downToTenSecond+'');
return digest.slice(2,11); // Only need a subset of the md5
+ },
+ isPageHidden: function() {
+ // Returns true if the page (browser tab) is hidden (e.g. inactive). Returns false otherwise.
+ return document.hidden || document.msHidden || document.webkitHidden;
}
});
@@ -3206,11 +3210,16 @@ GateOne.Base.update(GateOne.Terminal, {
v = go.Visual,
prefix = go.prefs.prefix,
term = termUpdateObj['term'],
+ ratelimiter = termUpdateObj['ratelimiter'],
prevScrollback = localStorage[prefix+"scrollback" + term],
terminalObj = go.terminals[term],
textTransforms = go.Terminal.textTransforms,
message = null;
// logDebug('GateOne.Utils.updateTerminalActionTest() termUpdateObj: ' + u.items(termUpdateObj));
+ if (ratelimiter) {
+ v.displayMessage("WARNING: The rate limiter was engaged on terminal " + term);
+ v.displayMessage("A Ctrl-c will be sent. If no response the process will be killed.");
+ }
try {
if (!terminalObj) {
// Terminal was just closed, ignore
View
@@ -145,6 +145,7 @@
# Import stdlib stuff
import re, logging, base64, copy, StringIO, codecs
+from datetime import datetime, timedelta
from collections import defaultdict
from itertools import imap, izip
@@ -615,7 +616,7 @@ def initialize(self, rows=24, cols=80, em_dimensions=None):
# Set the default window margins
self.top_margin = 0
self.bottom_margin = self.rows - 1
-
+ self.timeout_image = None
self.specials = {
self.ASCII_NUL: self.__ignore,
self.ASCII_BEL: self.bell,
@@ -691,7 +692,7 @@ def initialize(self, rows=24, cols=80, em_dimensions=None):
'm': self._set_rendition,
'n': self.__ignore, # <ESC>[6n is the only one I know of (request cursor position)
#'m': self.__ignore, # For testing how much CPU we save when not processing CSI
- 'p': self.terminal_reset, # TODO: "!p" is "Soft terminal reset". Also, "Set conformance level" (VT100, VT200, or VT300)
+ 'p': self.reset, # TODO: "!p" is "Soft terminal reset". Also, "Set conformance level" (VT100, VT200, or VT300)
'r': self._set_top_bottom, # DECSTBM (used by many apps)
'q': self.set_led_state, # Seems a bit silly but you never know
'P': self.delete_characters, # DCH Deletes the specified number of chars
@@ -871,14 +872,13 @@ def remove_all_callbacks(self, identifier):
except KeyError:
pass # No match, no biggie
- def terminal_reset(self, *args, **kwargs):
+ def reset(self, *args, **kwargs):
"""
Resets the terminal back to an empty screen with all defaults. Calls
:meth:`Terminal.callbacks[CALLBACK_RESET]` when finished.
.. note:: If terminal output has been suspended (e.g. via ctrl-s) this will not un-suspend it (you need to issue ctrl-q to the underlying program to do that).
"""
- logging.debug('terminal_reset(%s)' % args)
self.leds = {
1: False,
2: False,
@@ -1193,6 +1193,8 @@ def write(self, chars, special_checks=True):
for magic_header in magic.keys():
if magic_header.match(chars):
self.matched_header = magic_header
+ # TODO: Add a timeout here
+ self.timeout_image = datetime.now()
if self.image or self.matched_header:
self.image.extend(chars)
match = magic[self.matched_header].match(self.image)
@@ -1209,7 +1211,15 @@ def write(self, chars, special_checks=True):
self.write(before_chars, special_checks=False)
if after_chars:
self.write(after_chars, special_checks=False)
- return
+ # If we haven't got a complete image after one second something
+ # went wrong. Discard what we've got and restart.
+ one_second = timedelta(seconds=1)
+ if datetime.now() - self.timeout_image > one_second:
+ self.image = bytearray() # Empty it
+ self.matched_header = None
+ chars = _("Failed to decode image. Buffer discarded.")
+ else:
+ return
# Have to convert to unicode
try:
chars = unicode(chars.decode('utf-8', "handle_special"))
Oops, something went wrong.

0 comments on commit 84df9cc

Please sign in to comment.