为什么Python中的cmd.onecmd无法停止cmd.cmdloop?
在使用命令行(cmd)来运行Python时,我有一个叫做do_exit的方法,它返回True,这样可以让命令循环(cmdloop)退出。
我尝试调用 cmd.onecmd('exit')
,但是当命令 exit
执行时,命令循环并没有退出。
而且没有任何错误提示。do_exit方法确实被调用了,因为 Exiting me.
这句话确实被打印出来了。这让事情变得更加神秘。
我查看了 cmd模块
的源代码,但找不到问题出在哪里。考虑到 cmd.onecmd
可能会对所有命令进行调用,我觉得直接调用它应该是有效的。
我还尝试过用 cmd.postcmd
来处理 cmd.onecmd
的返回值,但那也没有效果。
具体的代码行在 quit_all
函数的底部附近。
#!/usr/bin/python
import cmd
from gi.repository import Gtk
import threading
class WindowWidget(object):
def __init__(self):
win = Gtk.Window()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
self.window = win;
def create_button(self):
self.button = Gtk.Button(label="Click Here")
self.button.connect("clicked", self.on_button_clicked)
self.window.add(self.button)
self.window.show_all()
def on_button_clicked(self, widget):
print 'something happened'
return
class InteractiveShell(cmd.Cmd):
#Simple command processor example.
prompt='>'
def __init__(self, gtk_object):
cmd.Cmd.__init__(self)
# or cmd.Cmd.__init__(self)
self.gtk_object = gtk_object
def do_greet(self, line):
print "hello"
def do_setbutton(self, line):
self.gtk_object.create_button()
def do_exit(self, line):
"""Close the gtk main loop, and return True to exit cmd."""
print 'Exiting me.'
return True
def postloop(self):
print 'Closing window.'
if Gtk.main_level() > 0:
Gtk.main_quit()
def worker(num, s):
"""thread worker function"""
#print 'Worker: %s' % num
s.cmdloop()
return
def worker2(num):
Gtk.main()
threads = []
if __name__ == '__main__':
ww = WindowWidget()
mainshell = InteractiveShell(ww)
threads = []
def quit_all(event, user_data):
print 'x out of window. Trying to exit shell.'
#offending line cmdloop won't exit
r = mainshell.onecmd('exit')
return False
ww.window.connect("delete-event", quit_all)
t = threading.Thread(target=worker, args=(1, mainshell))
threads.append(t)
t2 = threading.Thread(target=worker2, args=(2,))
threads.append(t2)
t.start()
t2.start()
编辑 在调整代码并从cmd对象内部运行 cmd.onecmd
方法后,我发现这个方法确实有效。显然,问题可能出在线程上。某些东西导致 exit
在 gtk事件
,特别是 delete-event
中无法正常工作。我仍然不知道具体问题是什么。
3 个回答
在Python3中,你可以这样做:
interactor.cmdqueue.append("exit\n") #Where interactor is the object(Cmd)
print("Please press Enter to continue...")
这里的意思是,cmdloop()
会在 input()
这个地方停下来(如果你把 raw_input
设置为 False,它会在 readline()
这个地方停)。不过,你可以简单地把命令添加到它用来回放的命令队列里。问题是,你需要按一下 Enter
键,才能让它通过 input()
的部分。这是有道理的,因为如果不这样,Cmd
就无法提供你所期望的流畅直观的交互体验。
我觉得我能理解问题出在哪里……但我没有简单的解决办法。如果在 cmdloop
中调用 onecommand('exit')
,那就相当于什么都没做。从 cmd
模块的角度来看,它会启动一个 cmdloop
,而这个 cmdloop
会不断调用命令,并查看这些命令的返回值。大概是这样的:
while True:
cr = do_cmd
if cr:
break
如果你在循环外调用 do_cmd
,那就没人关心,循环还是在等待。这正是你程序中发生的事情:你调用了 do_exit
,它执行了没有错误,但你并没有打断循环。
现在说说解决办法(我没有安装 Gtk,所以无法自己测试):
简单粗暴的方法:你可以把运行 cmdloop
的线程设为守护线程。当你想关闭程序时,先清理其他东西(比如窗口),然后退出。这个线程(和它的循环)应该被强制停止。
更好但更复杂的方法:不要把 sys.stdin
传给 cmd
,而是只用一个管道。用你窗口中的一个字段来获取输入命令,然后把它写入管道。当你想停止时,往管道里写 'exit'。循环会读取这个命令,执行你的 do_exit
方法,返回 True
然后退出。不过我得承认,cmd
模块的某些功能可能会失去一些吸引力……
因为我不知道你在应用中到底想做什么,所以我无法想出更有效的解决方案。也许你应该提供更多关于你想做的事情的整体结构的细节。
根据其他人的回答,我尝试把命令行线程变成
daemon
(后台线程),然后用sys.exit
退出程序。结果不行,窗口卡住了,我不得不强制关闭。我不知道为什么会这样,因为我还不太懂。我决定不再把线程变成daemon
。接着,按照另一个相关问题的建议,我尝试把
Gtk.main
放在主线程里,而不是单独的线程。Gtk窗口
打开了,但没有命令行界面。虽然这样不行,但……我把
Gtk.main
移到程序的最底部。现在一切都能打开了。我可以用sys.exit
关闭所有东西,但出现了另一个问题。终端输入是空白的。一开始我以为它不接受输入。经过在谷歌上查找,我发现有其他人也问过类似的问题,结果发现终端其实是可以接受输入的,只是看不见。按照建议,我尝试了
os.system('reset')
,现在可以用了。太好了!不过这个关闭方式很糟糕,窗口在终端输入已经重置后还会卡住几秒钟。我在网上继续寻找。在一个偏僻的论坛上,我找到了
stty echo
和stty sane
这两个终端命令。它们都能解决这个问题。我选择了os.system('stty sane')
。
现在一切都正常了。为了保持最新,我把os.system('stty sane')
改成了Popen('stty sane', shell=True)
。其他人说应用程序会修改终端设置,这会导致输入不回显。可能是命令行在搞鬼。stty sane
是个简单明了的解决方案。
这里是包含所有解决方案的代码。唉,事情比我想的简单多了。
#!/usr/bin/python
import cmd
from gi.repository import Gtk
import threading
"""
The bottom of this file has all the goodies for this solution.
Scroll down to see the cool stuff.
"""
class WindowWidget(object):
def __init__(self):
win = Gtk.Window()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
self.window = win;
def create_button(self):
self.button = Gtk.Button(label="Click Here")
self.button.connect("clicked", self.on_button_clicked)
self.window.add(self.button)
self.window.show_all()
def on_button_clicked(self, widget):
print 'something happened'
return
class InteractiveShell(cmd.Cmd):
#Simple command processor example.
prompt='>'
def __init__(self, gtk_object):
cmd.Cmd.__init__(self)
# or cmd.Cmd.__init__(self)
self.gtk_object = gtk_object
def do_greet(self, line):
print "hello"
def do_setbutton(self, line):
"""
Using GLib.idle_add to insert a callback into the Gtk loop.
This makes synchronizing the main thread where gtk is at, and
this second thread easier.
"""
GLib.idle_add(self.idle_setbutton)
def idle_setbutton(self):
self.gtk_object.create_button()
def do_exit(self, line):
"""Close the gtk main loop, and return True to exit cmd."""
print 'Exiting me.'
return True
def postloop(self):
print 'Closing window.'
if Gtk.main_level() > 0:
Gtk.main_quit()
def worker(s):
s.cmdloop()
return
#isn't required
#def worker2(num):
# Gtk.main()
if __name__ == '__main__':
ww = WindowWidget()
mainshell = InteractiveShell(ww)
#This list is superfluous
#threads = []
def quit_all(event, user_data):
print 'x out of window. Trying to exit shell.'
#I don't know if the next line is needed.
#It's here just in case.
Gtk.main_quit()
#offending line cmdloop won't exit
#but now it will because of sys.exit
r = mainshell.onecmd('exit')
#Problem found with sys.exit
#When this program closes terminal input is working, but
#it's blank. Here comes the fix.
#Works, but it is messy -> os.system('reset')
#Does not work -> Popen('reset', shell=True)
#The command 'stty sane' is the best way to keep the
#terminal from having blank input after closing.
#os.system('stty sane')
#Actually use Popen for this.
Popen('stty sane', shell=True)
#alternatively
#os.system('stty echo')
#sys.exit can only exit the whole program from the main thread.
sys.exit(0)
#Threads are closing, and so is the window.
#This is a clean close.
return False
ww.window.connect("delete-event", quit_all)
t = threading.Thread(target=worker, args=(mainshell))
#No need to create a thread for Gtk.main
#threads.append(t)
#t2 = threading.Thread(target=worker2, args=(2,))
#threads.append(t2)
t.start()
#t2.start()
#Gtk.main needs to be called after the second thread is started.
#This will keep Gtk from blocking the second thread.
#Gtk.main loop in the python main thread is easier to deal with
#to make sure that work for Gtk will get finished.
#Can use GLib.idle_add to insert functions, and methods into
#the Gtk loop.
Gtk.main()
我没有尝试从Gtk字段中获取输入。也许下次再试。