使用Python,如何在前台运行循环并在后台等待按键输入?

3

我知道这个主题在 这里 有相似的讨论,但我的设计要求稍微更为复杂。

我正在设计一个 CLI 脚本,将在 SSH 窗口中运行。该脚本将托管和执行在 Ubuntu 14.10 服务器上。它旨在以前台模式积极监控主机交换机上端口和客户端的当前状态。每30秒或由用户定义时,它将通过SNMP获取数据,然后刷新信息并显示到屏幕上。当等待下一次刷新时,有一个计时器指示何时再次查询设备以获取信息。

我想允许用户随时按特定键来更改输出视图或编辑关键变量。 (此功能类似于 Unix 的 top 命令。)例如,按下 t 键将请求输入期望在循环之间的秒数。 hmi 将切换显示 / 隐藏某些列。这些操作不会暂停计时器也不会退出循环,因为更改将应用于下一次刷新。 r 将强制立即刷新并应用更改。 qCtrl+C 将退出脚本。

主要活动将如下所示:

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

如果使用按键中断,它将表现如下:

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

我有些困惑。据我所知,我可能需要实现线程 - 我没有经验 - 因为仅使用while循环似乎不能满足我们的需求。我还不确定如何注入包含变量(例如计时器,显示格式标志)的对象的更改,因为它将不断被我们的循环使用。


你的代码将在哪个平台上运行? - Gabe
@Gabe 抱歉,我会在上面加上。Ubuntu 14.10服务器。 - Kamikaze Rusher
2个回答

1
这并不复杂,也不需要任何软件包。
它唯一的问题是在程序退出时需要恢复终端的正常状态。
即,如果程序崩溃,终端将无法恢复,用户将看不到自己正在输入什么。
但是,如果用户知道自己在做什么,可以强制重新启动shell,一切都会恢复正常。
当然,您可以更简单地使用此代码,并使用raw_input()完成操作。
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!

在实践中,程序不应该在不恢复终端正常状态的情况下退出,但有时会出现问题。

现在,您可以通过仅使用一个线程并将工作循环放入主线程中,并使用raw_input从用户获取命令来简化事情。 或者更好的是,将您的服务器客户端代码放在后台并在主线程中等待输入。

使用线程模块可能也更安全。

如果您使用asyncore模块,则每个客户端都将运行自己的代码,并且您的主线程将被占用于asyncore.loop()。 您可以重写它,即重新编写它以检查输入和其他您希望执行的操作,同时保持asyncore检查同步。 此外,如果负载较重,则需要覆盖其中一些nasty函数,因为其缓冲区固定为512字节(如果我没有记错的话)。 否则,这可能是您问题的一个不错的解决方案。

最后,只是为了明确起见,不回显用户输入的代码取自getpass模块并进行了适应。 只有一点是我的。


非常感谢!这正是我在寻找的。我会将其分解以了解每个细节,但到目前为止,我已经能够跟上并“理解”它。不过有一件事,您没有import os,这导致它无限循环并忽略Ctrl+C以退出。没有它,解释器认为os是全局变量。我已将其添加到我的代码中,但如果您可以修改您的答案以帮助其他可能遇到此问题的人,那将有所帮助,以防止他们崩溃自己的系统。一旦您这样做了,我将把它标记为我的问题的答案:D - Kamikaze Rusher
看到你添加了 import os。再次感谢您的回答,非常感激! - Kamikaze Rusher
1
很抱歉!我从我写的一个控制台播放器中提取了代码并进行了修改,但没有尝试它。所以,我错过了os模块的依赖。再次道歉!是的,如果一个线程中断,程序将继续无限运行,并且不允许您使用KeyboardInterrupt。因此,也许我们应该更改最后一个循环。(删除try语句)使用线程模块可以更好地解决此类错误。但概念仍然相同。 - Dalen
1
我编辑了答案并为你的麻烦点赞。我真的很粗心。再次抱歉! - Dalen
没关系。原始代码没有真正让机器崩溃的潜力,所以情况并不像我说的那么糟糕。可能我最终会实现线程模块,但对于我的脚本来说,这绝对是完美的选择。祝你好运! - Kamikaze Rusher
将循环中的sleep()删除并替换为pass,它会快速限制您计算机的通风口,并使您的房间变暖。 :D 我很高兴我能帮助到你。干杯! - Dalen

0

Ctrl+C很容易:它会抛出一个异常,你可以捕获它(因为当这种情况发生时,进程会收到一个信号)。

为了在等待时提供交互性,你应该考虑编写异步代码。Twisted经过时间的考验,功能强大,但有一定的学习曲线(在我看来)。还有asyncore,可能更容易入门,但更加有限,我不确定它是否处理你的用例。还有asyncio,但它只存在于Python 3.4中。


我觉得我不小心删除了一些信息。我是通过SNMP请求获取数据而不是HTTP。这可能会稍微影响你的答案。我已经改变了那个。你的一些参考资料看起来可以处理子程序。也许我应该将我的主活动循环设置为一个子进程,运行一次,返回给它的调用者,然后让调用者应用任何更改并再次运行它? - Kamikaze Rusher
我认为 SNMP 不会改变任何东西。Twisted 有原始的 TCP/UDP 协议,并且似乎有第三方的 Twisted SNMP 实现。 - Krumelur
我会研究那些第三方实现。我正在使用Net-SNMP项目中的“netsnmp”包(而不是“pySnmp”)。希望我能以类似的方式使它工作。 - Kamikaze Rusher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接