From 93a1d9c164a4a745e86382b7e09ae408927c7c38 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Thu, 29 Sep 2016 23:31:15 -0700 Subject: [PATCH] Run script in a new thread with timeout * Also fixes script output not sent to logger --- plexpy/config.py | 1 + plexpy/notifiers.py | 77 +++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/plexpy/config.py b/plexpy/config.py index e1e05ad4..0bec3373 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -509,6 +509,7 @@ _CONFIG_DEFINITIONS = { 'SCRIPTS_ON_PMSUPDATE_SCRIPT': (unicode, 'Scripts', ''), 'SCRIPTS_ON_CONCURRENT_SCRIPT': (unicode, 'Scripts', ''), 'SCRIPTS_ON_NEWDEVICE_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_TIMEOUT': (int, 'Scripts', 30), 'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''), 'TELEGRAM_ENABLED': (int, 'Telegram', 0), 'TELEGRAM_CHAT_ID': (str, 'Telegram', ''), diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index e1e6944c..f3301143 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -26,6 +26,7 @@ import requests import shlex import smtplib import subprocess +import threading import time import urllib from urllib import urlencode @@ -2116,6 +2117,7 @@ class Scripts(object): def __init__(self, **kwargs): self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.ps1', '.py', '.pyw', '.rb', '.sh') self.script_folder = plexpy.CONFIG.SCRIPTS_FOLDER + self.script_timeout = plexpy.CONFIG.SCRIPTS_TIMEOUT self.scripts = {'play': plexpy.CONFIG.SCRIPTS_ON_PLAY_SCRIPT, 'stop': plexpy.CONFIG.SCRIPTS_ON_STOP_SCRIPT, 'pause': plexpy.CONFIG.SCRIPTS_ON_PAUSE_SCRIPT, @@ -2160,6 +2162,48 @@ class Scripts(object): return scripts + def run_script(self, script): + output = error = '' + try: + def kill_script(process): + logger.warn(u"PlexPy Notifiers :: Script exceeded timeout limit of %d seconds. " + "Script killed." % self.script_timeout) + process.kill() + self.script_killed = True + + process = subprocess.Popen(script, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=plexpy.CONFIG.SCRIPTS_FOLDER) + + timer = threading.Timer(self.script_timeout, kill_script, (process,)) + self.script_killed = False + + try: + timer.start() + output, error = process.communicate() + status = process.returncode + finally: + timer.cancel() + + except OSError as e: + logger.error(u"PlexPy Notifiers :: Failed to run script: %s" % e) + return False + + if error: + err = '\n '.join([l for l in error.splitlines()]) + logger.error(u"PlexPy Notifiers :: Script error: \n %s" % err) + return False + + if output: + out = '\n '.join([l for l in output.splitlines()]) + logger.debug(u"PlexPy Notifiers :: Script returned: \n %s" % out) + + if not self.script_killed: + logger.info(u"PlexPy Notifiers :: Script notification sent.") + return True + def notify(self, subject='', message='', notify_action='', script_args=None, *args, **kwargs): """ Args: @@ -2231,31 +2275,10 @@ class Scripts(object): script.extend(script_args) logger.debug(u"PlexPy Notifiers :: Full script is: %s" % script) + logger.debug(u"PlexPy Notifiers :: Executing script in a new thread.") + thread = threading.Thread(target=self.run_script, args=(script,)).start() - try: - p = subprocess.Popen(script, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=plexpy.CONFIG.SCRIPTS_FOLDER) - - out, error = p.communicate() - status = p.returncode - - if out and status: - out = out.strip() - logger.debug(u"PlexPy Notifiers :: Script returned: %s" % out) - - if error: - error = error.strip() - logger.error(u"PlexPy Notifiers :: Script error: %s" % error) - return False - else: - logger.info(u"PlexPy Notifiers :: Script notification sent.") - return True - - except OSError as e: - logger.error(u"PlexPy Notifiers :: Failed to run script: %s" % e) - return False + return True def return_config_options(self): config_option = [{'label': 'Supported File Types', @@ -2365,6 +2388,12 @@ class Scripts(object): 'description': 'Choose the script for user new device.', 'input_type': 'select', 'select_options': self.list_scripts() + }, + {'label': 'Script Timeout', + 'value': self.script_timeout, + 'name': 'scripts_timeout', + 'description': 'The number of seconds to wait before killing the script.', + 'input_type': 'number' } ]