I've written a data collector in python 3.6 which saves some data in RAM and sends it to the cloud every minute or saves it to the disk if there is no internet connection. The application is running in a console window so everyone can see if it is running or if it throws some exceptions.
To prevent data loss I want to save the data when Windows gets shutdown. I've found several sources which state to either use win32api.SetConsoleCtrlHandler
(for example SetConsoleCtrlHandler does not get called on shutdown) or a hidden window and listen to WM_QUERYENDSESSION
(for example: Prevent windows shutdown from python)
But both methods don't work as expected. SetConsoleCtrlHandler
does get a signal if the console window gets closed but doesn't get a signal if the whole system gets shutdown.
The message loop with WM_QUERYENDSESSION
only works if I use pythonw.exe without a console window instead of python.exe, but I want to have a console window. I guess with the python console open the console kills my process before the message loop executes my graceful shutdown.
Does anyone have a working example on how to prevent a windows shutdown from within a python console?
I think I've found a suitable solution: I created my own small console application and hook into its message queue to catch the shutdown event. I haven't tested it much yet and I also don't know if this is a good solution, but maybe it's helpful for someone.
First here is the code for my simple console based on tkinter. It shows stdout in black and stderr in red:
# 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)
Then I created a class which hooks into the message queue of my console and manages the shutdown:
#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)
And here is some "main" code to start both classes and test it:
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()