python控制台延迟窗口关闭

时间:2019-10-29 14:12:04

标签: python python-3.x windows shutdown system-shutdown

我已经在python 3.6中编写了一个数据收集器,该数据收集器将一些数据保存在RAM中,每分钟发送到云中;如果没有互联网连接,则将其保存到磁盘中。该应用程序正在控制台窗口中运行,因此每个人都可以查看它是否正在运行或是否抛出某些异常。

为防止数据丢失,我想在Windows关闭时保存数据。我发现有几个来源声明使用win32api.SetConsoleCtrlHandler(例如SetConsoleCtrlHandler does not get called on shutdown)或隐藏窗口并收听WM_QUERYENDSESSION(例如:Prevent windows shutdown from python

但是两种方法都无法按预期工作。如果控制台窗口关闭,SetConsoleCtrlHandler会收到信号,但如果整个系统都关闭,则不会收到信号。 仅当我使用不带控制台窗口而不是python.exe的pythonw.exe而我想要一个控制台窗口时,带有WM_QUERYENDSESSION的消息循环才有效。我猜想使用python控制台打开时,控制台会在消息循环执行正常关机之前杀死我的进程。

有人在如何防止python控制台中的Windows关闭方面有有效的示例吗?

1 个答案:

答案 0 :(得分:1)

我认为我已经找到了合适的解决方案: 我创建了自己的小型控制台应用程序,并挂接到其消息队列中以捕获关闭事件。 我还没有对其进行太多测试,也不知道这是否是一个好的解决方案,但这也许对某人有用。

首先,这是我基于tkinter的简单控制台的代码。它以黑色显示stdout,以红色显示stderr:

# a simple console based on tkinter to display stdout and stderr
class SimpleConsole(object):

def __init__(self, name):
    self.root = Tk()
    self.root.title(name)
    self.init_ui()

def init_ui(self):
    self.text_box = Text(self.root, wrap='word', height = 11, width=50)
    self.text_box.grid(column=0, row=0, columnspan = 2, sticky='NSWE', padx=5, pady=5)
    self.text_box.tag_config('std', foreground="black")
    self.text_box.tag_config('err', foreground="red")
    self.text_box.pack(side=LEFT, fill=BOTH, expand = YES)
    self.text_box.yview()
    self.yscrollbar = Scrollbar(self.root, orient=VERTICAL, command=self.text_box.yview)
    self.yscrollbar.pack(side=RIGHT, fill=Y)
    self.text_box["yscrollcommand"] = self.yscrollbar.set
    sys.stdout = SimpleConsole.StdRedirector(self.text_box, "std")
    sys.stderr = SimpleConsole.StdRedirector(self.text_box, "err")
    self.update()

class StdRedirector(object):
    def __init__(self, text_widget, tag):
        self.text_space = text_widget
        self.tag = tag

    def write(self, string):
        self.text_space.insert('end', string, self.tag)
        self.text_space.see('end')

    def flush(self):
        pass

def update(self):
    self.root.update()

def get_window_handle(self):
    return int(self.root.wm_frame(), 16)

然后,我创建了一个类,该类可以挂接到控制台的消息队列中并管理关机:

#class to handle a graceful shutdown by hooking into windows message queue
class GracefulShutdown:
def __init__(self, handle):
    self.shutdown_requested = False
    self._shutdown_functions = []
    self.handle = handle

    try:
        if os.name == 'nt':

            # Make a dictionary of message names to be used for printing below
            self.msgdict = {}
            for name in dir(win32con):
                if name.startswith("WM_"):
                    value = getattr(win32con, name)
                    self.msgdict[value] = name

            # Set the WndProc to our function
            self.oldWndProc = win32gui.SetWindowLong(self.handle, win32con.GWL_WNDPROC, self.my_wnd_proc)
            if self.oldWndProc == 0:
                raise NameError("wndProc override failed!")

            self.message_map = {win32con.WM_QUERYENDSESSION: self.hdl_query_end_session,
                                win32con.WM_ENDSESSION: self.hdl_end_session,
                                win32con.WM_QUIT: self.hdl_quit,
                                win32con.WM_DESTROY: self.hdl_destroy,
                                win32con.WM_CLOSE: self.hdl_close}

            # pass a shutdown message to windows
            retval = windll.user32.ShutdownBlockReasonCreate(self.handle,c_wchar_p("I'm still saving data!"))
            if retval == 0:
                raise NameError("shutdownBlockReasonCreate failed!")
    except Exception as e:
        logging.exception("something went wrong during win32 shutdown detection setup")

#catches all close signals and passes it to our own functions; all other signals are passed to the original function
def my_wnd_proc(self, hwnd, msg, w_param, l_param):
    # Display what we've got.
    logging.debug(self.msgdict.get(msg), msg, w_param, l_param)

    # Restore the old WndProc.  Notice the use of wxin32api
    # instead of win32gui here.  This is to avoid an error due to
    # not passing a callable object.
    if msg == win32con.WM_DESTROY:
        win32api.SetWindowLong(self.handle,
        win32con.GWL_WNDPROC,
        self.oldWndProc)

    #simplify function for calling
    def call_window_proc_old():
        return win32gui.CallWindowProc(self.oldWndProc, hwnd, msg, w_param, l_param)

    #either call our handle functions or call the original wndProc
    return self.message_map.get(msg, call_window_proc_old)()


def hdl_query_end_session(self):
    logging.info("WM_QUERYENDSESSION received")
    self.shutdown_requested = True
    #we have to return 0 here to prevent the windows shutdown until our application is closed
    return 0

def hdl_end_session(self):
    logging.info("WM_ENDSESSION received")
    self.exit_gracefully()
    return 0

def hdl_quit(self):
    logging.info("WM_QUIT received")
    self.shutdown_requested = True
    return 0

def hdl_destroy(self):
    logging.info("WM_DESTROY received")
    return 0

def hdl_close(self):
    logging.info("WM_CLOSE received")
    self.shutdown_requested = True
    return 0

def exit_gracefully(self):
    logging.info("shutdown request received")
    self.shutdown_requested = True
    for func in self._shutdown_functions:
        try:
            func()
        except:
            logging.exception("Exception during shutdown function:")
    logging.info("shutdown request done, bye!")
    exit(0)

def add_cleanup_function(self, function):
    self._shutdown_functions.append(function)

这是一些“主要”代码来启动和测试两个类:

if __name__ == "__main__":
import time
from logging.handlers import RotatingFileHandler

#setup own console window
console = SimpleConsole("Test Shutdown")

#setup 3 loggers:
#log debug and info to stdout
#log warning and above to stderr
#log info and above to a file
logging.getLogger().setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging_path = 'graceful_shutdown_test.log'

rot_file_handler = RotatingFileHandler(logging_path, maxBytes=50 * 1024 * 1024, backupCount=5)
rot_file_handler.setFormatter(formatter)
rot_file_handler.setLevel(logging.INFO)
logging.getLogger().addHandler(rot_file_handler)

log_to_stdout = logging.StreamHandler(sys.stdout)
log_to_stdout.setLevel(logging.INFO)
log_to_stdout.addFilter(lambda record: record.levelno <= logging.INFO)
log_to_stdout.setFormatter(formatter)
logging.getLogger().addHandler(log_to_stdout)

log_to_stderr = logging.StreamHandler()
log_to_stderr.setLevel(logging.WARNING)
log_to_stderr.setFormatter(formatter)
logging.getLogger().addHandler(log_to_stderr)

logging.info("start shutdown test")

#init graceful shutdown with tkinter window handle
shutdown = GracefulShutdown(console.get_window_handle())

counter = 0
counterError = 0

#test cleanup function which runs if shutdown is requested
def graceful_shutdown():
    logging.info("start shutdown")
    time.sleep(15)
    logging.info("stop shutdown")
shutdown.add_cleanup_function(graceful_shutdown)

#main test loop
while not shutdown.shutdown_requested:
    console.update()
    counter += 1
    if counter > 50:
        logging.info("still alive")
        counter = 0

    counterError += 1
    if counterError > 150:
        logging.error("error for test")
        try:
            raise NameError("i'm a exception")
        except:
            logging.exception("exception found!")
        counterError = 0
    time.sleep(0.1)
shutdown.exit_gracefully()