6

我想用python处理控制台中的键盘事件。正在运行的脚本有一些持久的输出流,当管理员触发按键事件时,脚本会改变其输出内容。

我用如下代码完成了它(按'q'将触发输出更改),但有两个问题

  1. 我的输出空间增加了。调试后,我发现代码“tty.setraw(fd)”导致了这个问题,但我不知道如何解决它
  2. ctrl+c 不能再工作了(如果 # "tty.setraw(fd)", ctrl+c 会工作)

如果它太复杂,任何其他模块都可以做我想要的吗?我尝试了 curse 模块,似乎会冻结窗口输出并且无法在多线程中协调

#!/usr/bin/python
import sys
import select
import tty, termios
import threading
import time

def loop():

    while loop_bool:
        if switch:   
            output = 'aaaa'
        else:
            output = 'bbbb'
        print output
        time.sleep(0.2)


def change():
    global switch
    global loop_bool    
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    try: 
        while loop_bool:
            tty.setraw(fd)
            i,o,e = select.select([sys.stdin],[],[],1)
            if len(i)!=0:
                if i[0] == sys.stdin:
                    input = sys.stdin.read(1)

                    if input =='q':
                        if switch:
                            switch = False                                         
                        else: 
                            switch =  True


        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    except KeyboardInterrupt:

        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        loop_bool = False


try:   
    switch = True
    loop_bool = True    
    t1=threading.Thread(target=loop)
    t2=threading.Thread(target=change)        

    t1.start()
    t2.start()

    t1.join(1)
    t2.join(1)
except KeyboardInterrupt:

    loop_bool = False
4

1 回答 1

2

这可能取决于您使用的平台,甚至可能取决于您使用的终端模拟器,我不确定它是否能解决您的问题,但是……</p>

您应该能够在不调用 的情况下获得逐个字符的输入tty.setraw,只需将“规范模式”设置为关闭即可,您可以通过屏蔽ICANON.lflag 属性中的位来做到这一点tcgetattr()。您可能还需要设置 VMIN 或 VTIME 属性,但默认值应该已经正确。

有关详细信息,请参阅linux 手册页中的“规范和非规范模式”部分、 OS X 手册页中的“非规范模式输入处理”部分,或者如果您在不同的平台上,请参见同等内容。

将其编写为上下文管理器可能比进行显式清理更干净。特别是因为您现有的代码setraw每次都通过循环,并且只在最后恢复;理想情况下,它们应该成对匹配,并且使用with语句可以保证这一点。(另外,这样你就不需要在except子句和正常流程中重复自己。)所以:

@contextlib.contextmanager
def decanonize(fd):
    old_settings = termios.tcgetattr(fd)
    new_settings = old_settings[:]
    new_settings[3] &= ~termios.ICANON
    termios.tcsetattr(fd, termios.TCSAFLUSH, new_settings)
    yield
    termios.tcsetattr(fd, termios.TCSAFLUSH, old_settings)

现在:

def change():
    global switch
    global loop_bool

    with decanonize(sys.stdin.fileno()):
        try:
            while loop_bool:
                i,o,e = select.select([sys.stdin],[],[],1)
                if i and i[0] == sys.stdin:
                    input = sys.stdin.read(1)                    
                    if input =='q':
                        switch = not switch
        except KeyboardInterrupt:
            loop_bool = False

或者,也许您希望该with块位于较低级别(在 内部while,或至少在内部try)。

(PS,我将您的几行代码转换为等效但更简单的形式,以删除几层嵌套。)

YMMV,但这是我的 Mac 上的测试:

Retina:test abarnert$ python termtest.py 
aaaa
aaaa
aaaa
qbbbb
bbbb
bbbb
qaaaa
aaaa
aaaa
^CRetina:test abarnert$

这让我认为您可能想要关闭输入回显(您通过 执行new_settings[3] &= ~termios.ECHO),这意味着您可能想要decanonize用更通用的东西替换该函数,以临时设置或清除任意 termios 标志。(另外,如果tcgetattr返回 anamedtuple而不是 a会很好list,所以你可以做new_settings.lflag而不是new_settings[3],或者至少为属性索引提供符号常量。)

同时,根据您的评论,听起来 ^C 仅在前一两秒有效,并且与 s.timeout 中的超时有关join。这是有道理的——主线程只是启动两个线程,进行两次join(1)调用,然后结束。因此,在启动后的 2.something 秒内,它完成了所有工作——并离开了try:块——因此不再有任何方法让 aKeyboardInterrupt触发loop_bool = False并指示工作线程退出。

我不确定为什么你首先会在joins 上超时,以及当它们超时时应该发生什么,但有三种可能性:

  1. 您不想在 ^C 之前退出,并且超时不存在出于任何充分的理由。所以把它们拿出来。然后主线程将永远等待其他两个线程完成,并且它仍然在try块内,所以 ^C 应该能够设置loop_bool = False.

  2. 该应用程序应该在 2 秒后正常退出。(我猜你会更喜欢这对线程上的单个 join-any 或 join-all ,超时时间为 2 秒,但是因为 Python 没有任何简单的方法可以做到这一点,所以你按顺序加入了线程.) 在这种情况下,您希望loop_bool = False在超时结束后立即设置。所以只需将 更改exceptfinally.

  3. 超时总是应该足够大(大概这只是你真实应用程序的简化版本),如果你超过了超时,那是一个例外情况。以前的选项可能仍然有效,也可能无效。如果没有,设置daemon = True两个线程,当主线程完成时,它们将被杀死(不是很好地要求关闭)。(请注意,Windows 与 Unix 上的工作方式有点不同——尽管你可能不太关心这个应用程序的 Windows。更重要的是,所有文档都说“整个 Python 程序在没有活着的时候退出守护线程被留下,”所以你不应该指望任何守护线程能够进行任何清理,也不应该指望它们不会做任何清理工作。不要在守护线程中做任何可能留下临时文件、未写入关键日志消息等的事情。)

于 2013-01-03T06:49:41.960 回答