From 130ccada5aaeaded591ec2e54b6b08c78229f75b Mon Sep 17 00:00:00 2001 From: Spiros Georgaras Date: Wed, 16 Dec 2020 13:03:28 +0200 Subject: [PATCH] - Version 0.8.8.2 - Gracefully exit when the terminal is closed --- Changelog | 9 ++++ pyradio/__init__.py | 2 +- pyradio/main.py | 50 +++++++++-------- pyradio/player.py | 16 ++++-- pyradio/radio.py | 127 +++++++++++++++++++++++++++++++++----------- 5 files changed, 144 insertions(+), 60 deletions(-) diff --git a/Changelog b/Changelog index a26d2065..ac86f963 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,12 @@ +2020-12-18 s-n-g + * Version 0.8.8.2 + * Gracefully exit when the terminal is closed + +2020-12-14 s-n-g + * Version 0.8.8.1 + * Fixing (?) vlc terminataion on Windows + * Restarting radio-browser.info implementation + 2020-12-10 s-n-g * Starting 0.8.8 * Implementing "Paste to playlist" (\p) command diff --git a/pyradio/__init__.py b/pyradio/__init__.py index 7bb9f927..03809b9a 100644 --- a/pyradio/__init__.py +++ b/pyradio/__init__.py @@ -1,6 +1,6 @@ " pyradio -- Console radio player. " -version_info = (0, 8, 8, 1) +version_info = (0, 8, 8, 2) # Application state: # New stable version: '' diff --git a/pyradio/main.py b/pyradio/main.py index 9ff20900..2d9ae722 100644 --- a/pyradio/main.py +++ b/pyradio/main.py @@ -99,25 +99,6 @@ def shell(): pyradio_config.force_to_remove_lock_file = True sys.exit() - # set window title - if platform.startswith('win'): - import ctypes - try: - if pyradio_config.locked: - ctypes.windll.kernel32.SetConsoleTitleW("PyRadio: The Internet Radio player (Session Locked)") - else: - ctypes.windll.kernel32.SetConsoleTitleW("PyRadio: The Internet Radio player") - except: - pass - else: - try: - if pyradio_config.locked: - sys.stdout.write("\x1b]2;PyRadio: The Internet Radio player (Session Locked)\x07") - else: - sys.stdout.write("\x1b]2;PyRadio: The Internet Radio player\x07") - except: - pass - if args.show_config_dir: print('PyRadio config dir: "{}"'.format(pyradio_config.stations_dir)) sys.exit() @@ -211,13 +192,16 @@ def shell(): theme_to_use = pyradio_config.theme # Starts the radio TUI. - pyradio = PyRadio(pyradio_config, - play=args.play, - req_player=requested_player, - theme=theme_to_use) + pyradio = PyRadio( + pyradio_config, + play=args.play, + req_player=requested_player, + theme=theme_to_use + ) """ Setting ESCAPE key delay to 25ms Refer to: https://stackoverflow.com/questions/27372068/why-does-the-escape-key-have-a-delay-in-python-curses""" environ.setdefault('ESCDELAY', '25') + set_terminal_title() curses.wrapper(pyradio.setup) if pyradio.setup_return_status: print('\nThank you for using PyRadio. Cheers!') @@ -267,6 +251,26 @@ def print_playlist_selection_error(a_selection, cnf, ret, exit_if_malformed=True print('File type not supported') sys.exit(1) +def set_terminal_title(): + # set window title + if platform.startswith('win'): + import ctypes + try: + if pyradio_config.locked: + ctypes.windll.kernel32.SetConsoleTitleW("PyRadio: The Internet Radio player (Session Locked)") + else: + ctypes.windll.kernel32.SetConsoleTitleW("PyRadio: The Internet Radio player") + except: + pass + else: + try: + if pyradio_config.locked: + sys.stdout.write("\x1b]2;PyRadio: The Internet Radio player (Session Locked)\x07") + else: + sys.stdout.write("\x1b]2;PyRadio: The Internet Radio player\x07") + except: + pass + def open_conf_dir(cnf): import subprocess import os diff --git a/pyradio/player.py b/pyradio/player.py index b6284488..801cff64 100644 --- a/pyradio/player.py +++ b/pyradio/player.py @@ -1155,8 +1155,14 @@ def _sendCommand(self, command): if logger.isEnabledFor(logging.ERROR): logger.error(msg.format(command).strip(), exc_info=True) + def close_from_windows(self): + """ kill player instance when window console is closed """ + if self.process: + self.close() + self._stop() + def close(self): - """ exit pyradio (and kill player instance) """ + """ kill player instance """ self._no_mute_on_stop_playback() @@ -1413,7 +1419,7 @@ def pause(self): self._send_mpv_command('pause') def _stop(self): - """ exit pyradio (and kill mpv instance) """ + """ kill mpv instance """ self.stop_mpv_status_update_thread = True self._send_mpv_command('quit') os.system("rm " + self.mpvsocket + " 2>/dev/null"); @@ -1687,7 +1693,7 @@ def pause(self): self._sendCommand("p") def _stop(self): - """ exit pyradio (and kill mplayer instance) """ + """ kill mplayer instance """ self._sendCommand("q") self._icy_data = {} @@ -1844,7 +1850,7 @@ def pause(self): self._sendCommand("stop\n") def _stop(self): - """ exit pyradio (and kill vlc instance) """ + """ kill vlc instance """ logger.error('setting self.stop_win_vlc_status_update_thread = True') self.stop_win_vlc_status_update_thread = True if self.ctrl_c_pressed: @@ -1853,7 +1859,7 @@ def _stop(self): if self.process: logger.error('>>>> Terminating process') self._req('quit') - threading.Thread(target=self._remove_vlc_stdout_log_file, args=()).start() + threading.Thread(target=self._remove_vlc_stdout_log_file, args=()).start() else: self._sendCommand("shutdown\n") self._icy_data = {} diff --git a/pyradio/radio.py b/pyradio/radio.py index 0e30de9c..ed09728a 100644 --- a/pyradio/radio.py +++ b/pyradio/radio.py @@ -13,7 +13,7 @@ import logging import os import random -#import signal +import signal from sys import version as python_version, version_info, platform from os.path import join, basename, getmtime, getsize from os import remove @@ -183,6 +183,7 @@ def ll(self, msg): logger.error('DE p REGISTER_MODE: {0}, {1}, {2}'.format(*self.playlist_selections[2])) def __init__(self, pyradio_config, play=False, req_player='', theme=''): + self._system_asked_to_terminate = False self._cnf = pyradio_config self._theme = PyRadioTheme(self._cnf) if theme: @@ -694,6 +695,7 @@ def __displayBodyLine(self, lineNum, pad, station): self.bodyWin.chgat(lineNum, n, 1, sep_col) def run(self): + self._register_signals_handlers() if self.ws.operation_mode == self.ws.NO_PLAYER_ERROR_MODE: if self.requested_player: if ',' in self.requested_player: @@ -707,7 +709,6 @@ def run(self): except KeyboardInterrupt: pass else: - self._register_windows_handlers() # start update detection and notification thread if CAN_CHECK_FOR_UPDATES: @@ -751,7 +752,7 @@ def run(self): return except KeyboardInterrupt: if logger.isEnabledFor(logging.DEBUG): - logger.debug('Ctrl-C pressed... Exiting...') + logger.debug('Ctrl-C pressed... Terminating...') self.player.ctrl_c_pressed = True self.ctrl_c_handler(0, 0) break @@ -2894,7 +2895,7 @@ def to_time(secs): clean_date_files(files, -1) create_tadays_date_file(a_path) if logger.isEnabledFor(logging.INFO): - logger.info('detectUpdateThread: No update found. Will check again in {} days. Exiting...'.format(check_days)) + logger.info('detectUpdateThread: No update found. Will check again in {} days. Terminating...'.format(check_days)) break else: # PROGRAM DEBUG: set program's version @@ -2962,7 +2963,7 @@ def to_time(secs): connection_fail_count += 1 if connection_fail_count > 4: if logger.isEnabledFor(logging.ERROR): - logger.error('detectUpdateThread: Error: Too many connection failures. Exiting...') + logger.error('detectUpdateThread: Error: Too many connection failures. Terminating...') break delay(60, stop) @@ -3222,6 +3223,12 @@ def _fix_playlist_highlight_after_rename(self, old_file, new_file, copy_file, op return ret def keypress(self, char): + if self._system_asked_to_terminate: + """ Make sure we exit when signal received """ + if logger.isEnabledFor(logging.debug): + logger.debug('keypress: Asked to stop. Stoping...') + return -1 + if char in (ord('#'), curses.KEY_RESIZE): self._normal_mode_resize() self._do_display_notify() @@ -4019,7 +4026,7 @@ def keypress(self, char): char not in self._chars_to_bypass and \ char not in self._chars_to_bypass_for_search and \ char not in (ord('T'),)): - logger.error('DE \n\nExiting theme selector?\n\n') + logger.error('DE \n\nTerminating theme selector?\n\n') theme_id, save_theme = self._theme_selector.keypress(char) #if self._cnf.theme_not_supported: @@ -5426,7 +5433,7 @@ def _show_http_connection(self): """'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Windows only section ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''""" - def _register_windows_handlers(self): + def _register_signals_handlers(self): if platform.startswith('win'): """ disable close button """ import win32console, win32gui, win32con, win32api @@ -5436,8 +5443,12 @@ def _register_windows_handlers(self): if hMenu: try: win32gui.DeleteMenu(hMenu, win32con.SC_CLOSE, win32con.MF_BYCOMMAND) + if logger.isEnabledFor(logging.DEBUG): + logger.debug('SetConsoleCtrlHandler: close button disabled') except: - pass + if logger.isEnabledFor(logging.DEBUG): + logger.debug('SetConsoleCtrlHandler: failed to disable close button') + """ install handlers for exit / close signals""" try: result = win32api.SetConsoleCtrlHandler(self._windows_signal_handler, True) @@ -5451,37 +5462,91 @@ def _register_windows_handlers(self): logger.debug('SetConsoleCtrlHandler: Failed to register (with Exception)!!!') # Trying to catch Windows log-ogg, reboot, halt # No luck.... - #import signal - #try: - # signal.signal(signal.SIGINT, self._windows_signal_handler) - #except: - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug('SetConsoleCtrlHandler: Signal SIGINT failed to register (with Exception)!!!') - - #try: - # signal.signal(signal.SIGINT, self._windows_signal_handler) - #except: - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug('SetConsoleCtrlHandler: Signal SIGINT failed to register (with Exception)!!!') + # import signal + # try: + # signal.signal(signal.SIGINT, self._windows_signal_handler) + # except: + # if logger.isEnabledFor(logging.DEBUG): + # logger.debug('SetConsoleCtrlHandler: Signal SIGINT failed to register (with Exception)!!!') + + # try: + # signal.signal(signal.SIGINT, self._windows_signal_handler) + # except: + # if logger.isEnabledFor(logging.DEBUG): + # logger.debug('SetConsoleCtrlHandler: Signal SIGINT failed to register (with Exception)!!!') + + else: + self.handled_signals = { + 'SIGHUP': signal.SIGHUP, + 'SIGTERM': signal.SIGTERM, + 'SIGKIL': signal.SIGKILL, + } + self.def_signal_handlers = {} + try: + for a_sig in self.handled_signals.keys(): + self.def_signal_handlers[a_sig] = signal.signal( + self.handled_signals[a_sig], + self._linux_signal_handler + ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug('SetConsoleCtrlHandler: Handler for signal {} registered'.format(a_sig)) + except: + if logger.isEnabledFor(logging.DEBUG): + logger.debug('SetConsoleCtrlHandler: Failed to register handler for signal {}'.format(a_sig)) + + def _linux_signal_handler(self, a_signal, a_frame): + if self._system_asked_to_terminate: + return + self._system_asked_to_terminate = True + if logger.isEnabledFor(logging.INFO): + # logger.info('System asked me to terminate (signal: {})!!!'.format(list(self.handled_signals.keys())[list(self.handled_signals.values()).index(a_signal)])) + logger.info('My terminal got closed... Terminating...') + self._force_exit = True + self.stop_update_notification_thread = True + self.player.stop_timeout_counter_thread = True + if self.ws.operation_mode != self.ws.PLAYLIST_MODE: + if self._cnf.dirty_playlist: + self._cnf.save_playlist_file() + self.player.close() + self._cnf.save_config() + #self._wait_for_threads() + self._cnf.remove_session_lock_file() + for a_sig in self.handled_signals.keys(): + try: + signal.signal( + self.handled_signals[a_sig], + self.def_signal_handlers[a_sig] + ) + except: + pass def _windows_signal_handler(self, event): """ windows signal handler https://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/ """ - import win32con, win32api, signal + import win32con, win32api if event in (win32con.CTRL_C_EVENT, - win32con.CTRL_LOGOFF_EVENT, - win32con.CTRL_BREAK_EVENT, - win32con.CTRL_SHUTDOWN_EVENT, - win32con.CTRL_CLOSE_EVENT, - signal.SIGINT, - signal.SIGBREAK): + win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, + win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT, + signal.SIGINT, + signal.SIGBREAK): + if self._system_asked_to_terminate: + return + self._system_asked_to_terminate = True + if logger.isEnabledFor(logging.INFO): + logger.info('My console window got closed... Terminating...') self._force_exit = True - self.ctrl_c_handler(0,0) - if logger.isEnabledFor(logging.DEBUG): - logger.debug('Windows asked me to terminate!!') + self.player.close_from_windows() + self._cnf.save_config() + self._wait_for_threads() + self._cnf.remove_session_lock_file() + if self.ws.operation_mode != self.ws.PLAYLIST_MODE: + if self._cnf.dirty_playlist: + self._cnf.save_playlist_file() try: - result = win32api.SetConsoleCtrlHandler(self._windows_signal_handler, False) + win32api.SetConsoleCtrlHandler(self._windows_signal_handler, False) except: pass return False