From 549f1ebc8f514fc7b8c98aa90a63494aa708fd8c Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Fri, 27 Jun 2014 16:41:54 -0700 Subject: [PATCH 1/8] Attach a pty to each node's bash process This should enable node commands that are expecting a tty to behave better. --- mininet/cli.py | 15 ++++++++----- mininet/node.py | 58 +++++++++++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/mininet/cli.py b/mininet/cli.py index b432f7c..42a4d3d 100644 --- a/mininet/cli.py +++ b/mininet/cli.py @@ -55,7 +55,7 @@ class CLI( Cmd ): Cmd.__init__( self ) info( '*** Starting CLI:\n' ) - # Setup history if readline is available + # Set up history if readline is available try: import readline except ImportError: @@ -77,7 +77,7 @@ class CLI( Cmd ): node.sendInt() node.monitor() if self.isatty(): - quietRun( 'stty sane' ) + quietRun( 'stty echo sane intr "^C"' ) self.cmdloop() break except KeyboardInterrupt: @@ -352,8 +352,7 @@ class CLI( Cmd ): for arg in rest ] rest = ' '.join( rest ) # Run cmd on node: - builtin = isShellBuiltin( first ) - node.sendCmd( rest, printPid=( not builtin ) ) + node.sendCmd( rest ) self.waitForNode( node ) else: error( '*** Unknown command: %s\n' % line ) @@ -361,7 +360,7 @@ class CLI( Cmd ): # pylint: enable-msg=R0201 def waitForNode( self, node ): - "Wait for a node to finish, and print its output." + "Wait for a node to finish, and print its output." # Pollers nodePoller = poll() nodePoller.register( node.stdout ) @@ -379,7 +378,7 @@ class CLI( Cmd ): if False and self.inputFile: key = self.inputFile.read( 1 ) if key is not '': - node.write(key) + node.write( key ) else: self.inputFile = None if isReadable( self.inPoller ): @@ -391,8 +390,12 @@ class CLI( Cmd ): if not node.waiting: break except KeyboardInterrupt: + # There is an at least one race condition here, since + # it's possible to interrupt ourselves after we've + # read data but before it has been printed. node.sendInt() + # Helper functions def isReadable( poller ): diff --git a/mininet/node.py b/mininet/node.py index 783c949..ea0e369 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -45,6 +45,7 @@ Future enhancements: """ import os +import pty import re import signal import select @@ -118,16 +119,21 @@ class Node( object ): return # mnexec: (c)lose descriptors, (d)etach from tty, # (p)rint pid, and run in (n)amespace - opts = '-cdp' + opts = '-cd' if self.inNamespace: opts += 'n' - # bash -m: enable job control + # bash -m: enable job control, i: force interactive # -s: pass $* to shell, and make process easy to find in ps - cmd = [ 'mnexec', opts, 'bash', '-ms', 'mininet:' + self.name ] - self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, - close_fds=True ) - self.stdin = self.shell.stdin - self.stdout = self.shell.stdout + cmd = [ 'mnexec', opts, 'env', 'PS1=' + chr( 127 ), 'bash', + '--norc', '-mis', 'mininet:' + self.name ] + # Spawn a shell subprocess in a pseudo-tty, to disable buffering + # in the subprocess and insulate it from signals (e.g. SIGINT) + # received by the parent + master, slave = pty.openpty() + self.shell = Popen( cmd, stdin=slave, stdout=slave, stderr=slave, + close_fds=False ) + self.stdin = os.fdopen( master ) + self.stdout = self.stdin self.pid = self.shell.pid self.pollOut = select.poll() self.pollOut.register( self.stdout ) @@ -141,6 +147,14 @@ class Node( object ): self.lastPid = None self.readbuf = '' self.waiting = False + # Wait for prompt + while True: + data = self.read( 1024 ) + if chr( 127 ) in data: + break + self.pollOut.poll() + self.waiting = False + self.cmd( 'stty -echo' ) def cleanup( self ): "Help python collect its garbage." @@ -205,7 +219,7 @@ class Node( object ): args: command and arguments, or string printPid: print command's PID?""" assert not self.waiting - printPid = kwargs.get( 'printPid', True ) + printPid = kwargs.get( 'printPid', False ) # Allow sendCmd( [ list ] ) if len( args ) == 1 and type( args[ 0 ] ) is list: cmd = args[ 0 ] @@ -219,28 +233,17 @@ class Node( object ): # Replace empty commands with something harmless cmd = 'echo -n' self.lastCmd = cmd - printPid = printPid and not isShellBuiltin( cmd ) - if len( cmd ) > 0 and cmd[ -1 ] == '&': - # print ^A{pid}\n{sentinel} - cmd += ' printf "\\001%d\n\\177" $! \n' - else: - # print sentinel - cmd += '; printf "\\177"' - if printPid and not isShellBuiltin( cmd ): - cmd = 'mnexec -p ' + cmd + if printPid and not isShellBuiltin( cmd ): + cmd = 'mnexec -p ' + cmd self.write( cmd + '\n' ) self.lastPid = None self.waiting = True - def sendInt( self, sig=signal.SIGINT ): + def sendInt( self, intr=chr( 3 ) ): "Interrupt running command." - if self.lastPid: - try: - os.kill( self.lastPid, sig ) - except OSError: - pass + self.write( intr ) - def monitor( self, timeoutms=None ): + def monitor( self, timeoutms=None, findPid=True ): """Monitor and return the output of a command. Set self.waiting to False if command has completed. timeoutms: timeout in ms or None to wait indefinitely.""" @@ -248,7 +251,7 @@ class Node( object ): data = self.read( 1024 ) # Look for PID marker = chr( 1 ) + r'\d+\n' - if chr( 1 ) in data: + if findPid and chr( 1 ) in data: markers = re.findall( marker, data ) if markers: self.lastPid = int( markers[ 0 ][ 1: ] ) @@ -317,7 +320,10 @@ class Node( object ): # Shell requires a string, not a list! if defaults.get( 'shell', False ): cmd = ' '.join( cmd ) - return Popen( cmd, **defaults ) + old = signal.signal( signal.SIGINT, signal.SIG_IGN ) + popen = Popen( cmd, **defaults ) + signal.signal( signal.SIGINT, old ) + return popen def pexec( self, *args, **kwargs ): """Execute a command using popen From 82e0e9f38f7d7d8c9582ac668e9b38429d823b47 Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Tue, 1 Jul 2014 01:49:10 -0700 Subject: [PATCH 2/8] Avoid overhead of another process (env) in startShell() --- mininet/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mininet/node.py b/mininet/node.py index ea0e369..d977c61 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -124,8 +124,9 @@ class Node( object ): opts += 'n' # bash -m: enable job control, i: force interactive # -s: pass $* to shell, and make process easy to find in ps - cmd = [ 'mnexec', opts, 'env', 'PS1=' + chr( 127 ), 'bash', - '--norc', '-mis', 'mininet:' + self.name ] + # prompt is set to sentinel chr( 127 ) + os.environ[ 'PS1' ] = chr( 127 ) + cmd = [ 'mnexec', opts, 'bash', '--norc', '-mis', 'mininet:' + self.name ] # Spawn a shell subprocess in a pseudo-tty, to disable buffering # in the subprocess and insulate it from signals (e.g. SIGINT) # received by the parent From 355696f3dd8798b8a0367fc26104c9b66af2700e Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Tue, 1 Jul 2014 17:55:14 -0700 Subject: [PATCH 3/8] Don't set self.waiting twice --- mininet/node.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index d977c61..848ba9e 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -147,7 +147,6 @@ class Node( object ): self.lastCmd = None self.lastPid = None self.readbuf = '' - self.waiting = False # Wait for prompt while True: data = self.read( 1024 ) From 771850b9cfc9b5c8b790edcd152bae3b4e85ea92 Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Tue, 1 Jul 2014 18:04:44 -0700 Subject: [PATCH 4/8] Possibly faster check for sentinel. --- mininet/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index 848ba9e..50f92e6 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -150,7 +150,7 @@ class Node( object ): # Wait for prompt while True: data = self.read( 1024 ) - if chr( 127 ) in data: + if data[ -1 ] == chr( 127 ): break self.pollOut.poll() self.waiting = False From 16ddf6560a68438fd0b35730520926fbe8c1b1fd Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Mon, 7 Jul 2014 21:51:07 -0700 Subject: [PATCH 5/8] Fix findPid since pty uses \r\n as line ending --- mininet/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index 50f92e6..9db22b4 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -250,7 +250,7 @@ class Node( object ): self.waitReadable( timeoutms ) data = self.read( 1024 ) # Look for PID - marker = chr( 1 ) + r'\d+\n' + marker = chr( 1 ) + r'\d+' if findPid and chr( 1 ) in data: markers = re.findall( marker, data ) if markers: From c49b216c1f5f7899440fbd17f4ed3d53abce4cbc Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Mon, 7 Jul 2014 21:55:34 -0700 Subject: [PATCH 6/8] Set default printPid back to True --- mininet/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index 9db22b4..9e50319 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -219,7 +219,7 @@ class Node( object ): args: command and arguments, or string printPid: print command's PID?""" assert not self.waiting - printPid = kwargs.get( 'printPid', False ) + printPid = kwargs.get( 'printPid', True ) # Allow sendCmd( [ list ] ) if len( args ) == 1 and type( args[ 0 ] ) is list: cmd = args[ 0 ] From e9013d761fe8f04607b53e42df496d8b6f3f3455 Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Mon, 7 Jul 2014 21:58:30 -0700 Subject: [PATCH 7/8] Fix pid regex to eat \r\n --- mininet/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index 9e50319..258ce84 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -250,7 +250,7 @@ class Node( object ): self.waitReadable( timeoutms ) data = self.read( 1024 ) # Look for PID - marker = chr( 1 ) + r'\d+' + marker = chr( 1 ) + r'\d+\r\n' if findPid and chr( 1 ) in data: markers = re.findall( marker, data ) if markers: From 3b24bd7abd840479eeaedc63a4ee46c33853014f Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Tue, 8 Jul 2014 02:22:02 -0700 Subject: [PATCH 8/8] Restore non-mnexec pid detection for background commands --- mininet/node.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mininet/node.py b/mininet/node.py index 258ce84..7bb80ba 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -234,7 +234,11 @@ class Node( object ): cmd = 'echo -n' self.lastCmd = cmd if printPid and not isShellBuiltin( cmd ): - cmd = 'mnexec -p ' + cmd + if len( cmd ) > 0 and cmd[ -1 ] == '&': + # print ^A{pid}\n so monitor() can set lastPid + cmd += ' printf "\\001%d\n" $! \n' + else: + cmd = 'mnexec -p ' + cmd self.write( cmd + '\n' ) self.lastPid = None self.waiting = True