3

I understand that there are similar topics on this (such as here) but my intended design is a little more complex.

I'm designing a CLI script that will be run in an SSH window. The script will be hosted and executed on an Ubuntu 14.10 server. It's intended to actively monitor, in the foreground, the current status of ports and clients on a host switch. Every 30 seconds or as defined by the user it will fetch data via SNMP and then refresh information and display it to the screen. When it's waiting for the next refresh there is a timer indicating when it will query the device again for information.

I want to allow the user to press specific keys to change the output view or edit key variables at any time. (The functionality is similar to the Unix top.) For example, pressing t would request them to enter a number of seconds desired between loops. h, m, or i would toggle showing/hiding certain columns. These would not pause the timer nor exit the loop since changes would be applied at the next refresh. r would force an immediate refresh and apply changes. q or Ctrl+C would exit the script.

The primary activity would look like this:

Query loop <-----------------------------
     |                                   |
     |                              Process Data
     |                                   ^
     |                                   |
     v                               Query Data    #SNMPBULKWALK
   Timer <-------------                  ^
     | |               |                 |          
     | |        Check time remaining     |
     | |               ^                 |
     | |_______________|                 |
     |___________________________________|

With key-press interrupts it would act like this:

Query loop <----------------------                      
    |                             |        ???<---Change variables
    |                        (Continue)                  ^
    V                             |                      |
  Timer <---------          !!INTERRUPT!!---------> Identify key
    | |           |               ^
    | |  Check time remaining     |
    | |           ^               |
    | |___________|               |
    |_____________________________|

I'm kind of stumped here. I'm led to believe that I'll probably need to implement threading - which I do not have experience with - as a while loop by itself doesn't seem to satisfy what we need. I'm also unsure of how to inject the changes to the object that contains the variables (e.g. timer, flags for display formatting) since it will be constantly used by our loop.

2
  • What platform will your code be running on? Commented Jul 21, 2015 at 23:03
  • @Gabe Sorry, I'll include that above. Ubuntu 14.10 Server Commented Jul 21, 2015 at 23:03

2 Answers 2

1

It's nothing complicated, and nothing requiring any packages.

Its only problem is that it requires restoring terminal back to normal on program exit.

I.e. If program crashes the terminal will not be restored and user won't see what he is typing.

But if user knows what he is doing, he can force restart the shell and everything will be back to normal.

Of corse, you can use this code in easier manner and use raw_input() to do the stuff.

from thread import start_new_thread as thread
from time import sleep
# Get only one character from stdin without echoing it to stdout
import termios
import fcntl
import sys
import os
fd = sys.stdin.fileno()
oldterm, oldflags = None, None

def prepareterm ():
    """Turn off echoing"""
    global oldterm, oldflags
    if oldterm!=None and oldflags!=None: return
    oldterm = termios.tcgetattr(fd)
    newattr = oldterm[:] # Copy of attributes to change
    newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
    termios.tcsetattr(fd, termios.TCSANOW, newattr)
    oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)

def restoreterm ():
    """Restore terminal to its previous state"""
    global oldterm, oldflags
    if oldterm==None and oldflags==None: return
    termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
    oldterm, oldflags = None, None

def getchar():
    """Get character from stdin"""
    prepareterm()
    while 1:
        try:
            c = sys.stdin.read(1)
            break
        except IOError:
            try: sleep(0.001)
            except: restoreterm(); raise KeyboardInterrupt
    restoreterm()
    return c

def control ():
    """Waits for keypress"""
    global running, done
    while running:
        c = getchar().lower()
        print "Keypress:", c
        if c=="q": print "Quitting!"; running = 0; break
    done += 1

def work ():
    """Does the server-client part"""
    global done
    while running:
        # Do your stuff here!!!
        # Just to illustrate:
        print "I am protending to work!\nPress Q to kill me!"
        sleep(1)
    print "I am done!\nExiting . . ."
    done += 1

# Just to feel better
import atexit
atexit.register(restoreterm)

# Start the program
running = 1
done = 0
thread(work, ())
thread(control, ())
# Block the program not to close when threads detach from the main one:
while running:
    try: sleep(0.2)
    except: running = 0
# Wait for both threads to finish:
while done!=2:
    try: sleep(0.001)
    except: pass # Ignore KeyboardInterrupt
restoreterm() # Just in case!

In practice program should never be able to exit without restoring the terminal to normal, but shit happens.

Now, you can simplify things by using only one thread and your work loop put in the main thread, and use raw_input to acquire commands from user. Or maybe even better, put your server-client code in the background and wait for input in main thread.

It will also probably be safer to use threading module instead of raw threads.

If you use asyncore module, you will get each client running for its own, and your main thread will be occupied with the asyncore.loop(). You can override it, I.e. rewrite it to check for input and other things you wish to do, while keeping asyncore checks synchronized. Also, heavy loads require to override some nasty functions inside it, because its buffer is fixed to 512 bytes, if I am not mistaken. Otherwise, it may be a nice solution for your problem.

And, lastly, just to be clear, code for no echoing user input is taken and adapted from getpass module. Just a little bit there is mine.

Sign up to request clarification or add additional context in comments.

6 Comments

Thank you so much! This is exactly what I'm looking for. I'll be breaking it down to understand everything to the last detail but thus far I've been able to keep up and "get it." One thing though, you didn't import os which caused it to loop indefinitely and ignore Ctrl+C to break away. Without it the interpreter thought os was a global variable. I've added it to my code but if you can please modify your answer for others who may stumble upon this it may help prevent them from crashing their own system. I'll mark this as the answer to my question once you've done so :D
Saw that you added import os. Thanks again for your answer, much appreciated!
I am sorry! I extracted the code out of one console player I wrote and modified it without trying it. So, I missed the os module dependency. Sorry again! Yes, if one thread breaks program will continue to run indefinitely and will not allow you the KeyboardInterrupt. So maybe we should change the last loop. (remove try statement) Such errors can be better addressed by using threading module. But concept remains the same.
I edited the answer and up voted you for your trouble. I was really careless. Sorry again!
No worries. The original code did not have the potential to really bring a machine down so it wasn't as bad as I stated. It's likely I'll end up implementing the threading module far down the road but for how simple my script will be this is absolutely perfect. Best of luck!
|
0

Ctrl+C is easy: it will throw an exception that you can catch (because the process is sent a signal when this happens).

For providing interactivity while waiting, you should look writing asynchronous code. Twisted is time-tested and capable, but has a slight learning curve (IMHO). There is also asyncore that might be easier to get started with, but more limited and I am not sure it handles your use case. There is also asyncio, but it only exists in Python 3.4.

3 Comments

Shoot, I think I accidentally deleted some info. I'm fetching data via SNMP requests not HTTP. That may slightly affect your answer. I've changed that now. Some of your references look like they can handle subroutines. Perhaps I should set my main activity loop as a subprocess that runs once, returns to its caller, then let the caller apply any changes and run it again?
I don't think SNMP changes anything. Twisted has raw TCP/UDP protocols and there seems to be third-party SNMP implementations for Twisted.
I'll look into those third-party implementations. I'm using the netsnmp package from the Net-SNMP project (as opposed to pySnmp). Hopefully I can get it to work in a similar fashion.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.