This module provides a Multiplex class that can perform the following:
- Fork a child process that opens a given terminal program.
- Read and write data to and from the child process.
- Log the output of the child process to a file and/or syslog.
The Multiplex class is meant to be used in conjunction with a running Tornado IOLoop instance. It can be instantiated from within your Tornado application like so:
multiplexer = termio.Multiplex(
'nethack',
tmpdir='/tmp',
log_path='/var/log/myapp',
user='bsmith@CORP',
term_num=1,
syslog=True
)
Then multiplexer can create and launch a new controlling terminal (tty) running the given command (e.g. 'nethack'):
env = {
'PATH': os.environ['PATH'],
'MYVAR': 'foo'
}
fd = multiplexer.create(80, 24, env=env)
# The fd is returned from create() in case you want more low-level control.
Input and output from the controlled program is asynchronous and gets handled via IOLoop. It will automatically write all output from the terminal program to an instance of self.terminal_emulator (which defaults to Gate One's terminal.Terminal). So if you want to perform an action whenever the running terminal application has output (like, say, sending a message to a client) you'll need to attach a callback:
def screen_update():
'Called when new output is ready to send to the client'
output = multiplexer.dumplines()
socket_or_something.write(output)
multiplexer.callbacks[multiplexer.CALLBACK_UPDATE] = screen_update
In this example, screen_update() will write() the output of multiplexer.dumplines() to socket_or_something whenever the terminal program has some sort of output. You can also make calls directly to the terminal emulator (if you're using a custom one):
def screen_update():
output = multiplexer.term.my_custom_func()
whatever.write(output)
Writing characters to the controlled terminal application is pretty straightforward:
multiplexer.proc_write('some text')
Typically you'd pass in keystrokes or commands from your application to the underlying program this way and the screen/terminal emulator would get updated automatically. If using Gate One's Terminal() you can also attach callbacks to perform further actions when more specific situations are encountered (e.g. when the window title is set via that respective escape sequence):
def set_title():
'Hypothetical title-setting function'
print("Window title was just set to: %s" % multiplexer.term.title)
multiplexer.term.callbacks[multiplexer.CALLBACK_TITLE] = set_title
Used in conjunction with codecs.register_error, will replace special ascii characters such as 0xDA and 0xc4 (which are used by ncurses) with their Unicode equivalents.
The Multiplex class takes care of forking a child process and provides methods for reading/writing to it. It also creates an instance of tornado.ioloop.IOLoop that listens for events on the spawned terminal application and updates self.proc[fd]['term'] with any changes.
Creates a new virtual terminal (tty) and executes self.cmd within it. Also sets up our read/write callback and attaches them to Tornado's IOLoop.
Sets self.alive to False
NOTE: This is actually important as it allows controlling processes to see if the multiplexer is still alive or not (so they don't have to enumerate the process table looking for a particular pid).
Tells the running terminal program to redraw the screen by executing a window resize event (using its current dimensions) and writing a ctrl-l.
Kill the child process associated with the given file descriptor (fd).
NOTE: If dtach is being used this only kills the dtach process.
Writes chars to self.term and also takes care of logging to self.log_path (if set) and/or syslog (if self.syslog is True).
NOTE: This kind of logging doesn't capture user keystrokes. This is intentional as we don't want passwords winding up in the logs.
Read in the output of the process associated with fd and write it to self.term.
This method will also keep an eye on the output rate of the underlying terminal application. If it goes to high (which would gobble up CPU) it will engage a rate limiter. So if someone thinks it would be funny to run 'top' with a refresh rate of 0.01 they'll really only be getting updates every ~2 seconds (and it won't bog down the server =).
NOTE: This method is not meant to be called directly... The IOLoop should be the one calling it when it detects an io_loop.READ event.