# File: scripting.py # Library: DOPAL - DO Python Azureus Library # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details ( see the COPYING file ). # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' This module is designed to provide an 'environment' that allows small scripts to be written without having to deal with the setting up and exception handling that you would normally have to deal with. It also tries to make it straight-forward and distribute scripts without requiring any modification by another user to get it working on their system (most common change would be to personalise the script to work with a user's particular connection setup). This module provides simple functionality for scripts - data persistency, error handling and logging - it even provides a mechanism for sending alerts to the user to be displayed in Azureus (via "Mr Slidey"). There are two main functions provided here: - C{L{ext_run}} - which provides all the main functionality; and - C{L{run}} - which calls ext_run with the default settings, but allows these arguments to be modified through command line arguments. The following features are provided by this module: - B{Automatic connection setup} - Default connection settings can be set by running this module (or any script which uses the run method) with the C{--setup-connection} command line argument. This will provide the user with an input prompt to enter connection values, and store it an appropriate directory (see L{determine_configuration_directory}). That data is then used for all scripts using that module. - B{Data Persistency} - You are provided with access to methods to save and load a pickleable object - the module keeps the data stored in a unique data directory based on the script's name. - B{Logging (local)} - A logging system is initialised for the script to log any messages to - by default, logging to a file in the data directory. - B{Logging (remote)} - A LoggerChannel is set up to provide the ability to send log messages to Azureus through it's own logging mechanism. It also provides the ability to send alerts to the user (via Mr Slidey). - B{Pause on exit} - The module provides behaviour to pause whenever a script has finished execution, either in all cases, or only if an error has occurred. This makes it quite useful if you have the script setup to run in a window, which closes as soon as the script has terminated. When writing a script, it should look like this:: def script_function(env): ... # Do something here. if __name__ == '__main__': import dopal.scripting dopal.scripting.run("functionname", script_function) where "script_function" is the main body of the script (which takes one argument, a C{L{ScriptEnvironment}} instance) and "functionname" which is used to define the script (in terms of where persistent data is sent), and what the script is called when sending alerts to Azureus. ''' # Python 2.2 compatibility. from __future__ import generators import os import os.path _default_config_dir = None def determine_configuration_directory(mainname='DOPAL Scripts', subname=None, create_dir=True, preserve_case=False): ''' Determines an appropriate directory to store application data into. This function will look at environmental settings and registry settings to determine an appropriate directory. The locations considered are in order: - The user's home, as defined by the C{home} environment setting. - The user's application directory, as determined by the C{win32com} library. - The user's application directory, as determine by the C{_winreg} library. - The user's application directory, as defined by the C{appdata} environment setting. - The user's home, as defined by the C{homepath} environment setting (and if it exists, the C{homedrive} environment setting. - The user's home, as defined by the C{os.path.expanduser} function. - The current working directory. (Note: this order may change between releases.) If an existing directory can be found, that will be returned. If no existing directory is found, then this function will try to create the directory in the most preferred location (based on the order of preference). If that fails - no existing directory was found and no directory could be created, then an OSError will be raised. If create_dir is False and no existing directory can be found, then the most preferred candidate directory will be returned. The main argument taken by this function is mainname. This should be a directory name which is suitable for a Windows application directory (e.g. "DOPAL Scripts"), as opposed to something which resembles more Unix-based conventions (e.g. ".dopal_scripts"). This function will convert the C{mainname} argument into a Unix-style filename automatically in some cases (read below). You can set the C{preserve_case} argument to C{True} if you want to prevent automatic name conversation of this argument to take place. The C{subname} argument is the subdirectory which gets created in the main directory. This name will be used literally - no translation of the directory name will occur. When this function is considering creating or locating a directory inside a 'home' location, it will use a Unix-style directory name (e.g. ".dopal_scripts"). If it is considering an 'application' directory, it will use a Windows-style directory name (e.g. "DOPAL Scripts"). If it considers a directory it is unable to categorise (like the current working directory), it will use a Windows-style name on Windows systems, or a Unix-style name on all other systems. @param mainname: The main directory name to store data in - the default is C{"DOPAL Scripts"}. This value cannot be None. @param subname: The subdirectory to create in the main directory - this may be C{None}. @param create_dir: Boolean value indicating whether we should create the directory if it doesn't already exist (default is C{True}). @param preserve_case: Indicates whether the value given in C{mainname} should be taken literally, or whether name translation can be performed. Default is C{False}. @return: A directory which matches the specification given. This directory is guaranteed to exist, unless this function was called with C{create_dir} being False. @raise OSError: If C{create_dir} is C{True}, and no appropriate directory could be created. ''' # If we have an application data directory, then we will prefer to use # that. We will actually iterate over all directories that we consider, and # return the first directory we find. If we don't manage that, we'll create # one in the most appropriate directory. We'll also try to stick to some # naming conventions - using a dot-prefix for home directories, using # normal looking names in application data directories. # # Code is based on a mixture of user.py and homedirectory.py from the # pyopengl library. # Our preferred behaviour - existance of a home directory, and creating a # .dopal_scripts directory there. if not preserve_case: app_data_name = mainname home_data_name = '.' + mainname.lower().replace(' ', '_') import sys if sys.platform == 'win32': unknown_loc_name = app_data_name else: unknown_loc_name = home_data_name else: app_data_name = home_data_name = unknown_loc_name = mainname if subname: app_data_name = os.path.join(app_data_name, subname) home_data_name = os.path.join(home_data_name, subname) unknown_loc_name = os.path.join(unknown_loc_name, subname) def suggested_location(): # 1) Test for the home directory. if os.environ.has_key('home'): yield os.environ['home'], home_data_name # 2) Test for application data - using win32com library. try: from win32com.shell import shell, shellcon yield shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0), app_data_name except Exception, e: pass # 3) Test for application data - using _winreg. try: import _winreg key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") path = _winreg.QueryValueEx(key, 'AppData')[0] _winreg.CloseKey(key) yield path, app_data_name except Exception, e: pass # 4) Test for application data - using environment settings. if os.environ.has_key('appdata'): yield os.environ['appdata'], app_data_name # 5) Test for home directory, using other environment settings. if os.environ.has_key('homepath'): if os.environ.has_key('homedrive'): yield os.path.join(os.environ['homedrive'], os.environ['homepath']), home_data_name else: yield os.environ['homepath'], home_data_name # 6) Test for home directory, using expanduser. expanded_path = os.path.expanduser('~') if expanded_path != '~': yield expanded_path, home_data_name # 7) Try the current directory then. yield os.getcwd(), unknown_loc_name # This will go through each option and choose what directory to choose. # It will keep yielding suggestions until we've decided what we want to # use. suggested_unmade_paths = [] for suggested_path, suggested_name in suggested_location(): full_suggested_path = os.path.join(suggested_path, suggested_name) if os.path.isdir(full_suggested_path): return full_suggested_path suggested_unmade_paths.append(full_suggested_path) # Return the first path we're able to create. for path in suggested_unmade_paths: # If we don't want to create a directory, just return the first path # we have dealt with. try: os.makedirs(path) except OSError, e: pass else: # Success! if os.path.isdir(path): return path # If we get here, then there's nothing we can do. We gave it our best shot. raise OSError, "unable to create an appropriate directory" # Lazily-generated attribute stuff for ScriptEnvironment, taken from here: # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/363602 class _lazyattr(object): def __init__(self, calculate_function): self._calculate = calculate_function def __get__(self, obj, typeobj=None): if obj is None: return self value = self._calculate(obj) setattr(obj, self._calculate.func_name, value) return value # # Methods used for saving and loading data. # class ScriptEnvironment(object): ''' The ScriptEnvironment class contains values and methods useful for a script to work with. @ivar name: The name of the script. @ivar filename: The filename (no directory information included) of where persistent data for this object should be stored - default is C{data.dpl}. If you want to set a different filename, this value should be set before any saving or loading of persistent data takes place in the script. @ivar connection: The AzureusObjectConnection to work with. The connection should already be in an established state. connection may be None if ext_run is configured that way. @ivar logger: The logger instance to log data to. May be C{None}. @ivar log_channel: The logger channel object to send messages to. May be C{None}. For convenience the L{alert} method is available on ScriptEnvironment messages. @ivar default_repeatable_alerts: Indicates whether alerts are repeatable or not by default. This can be set explicitly on the object, but it can also be overridden when calling the L{alert} method. In most cases, this value will be C{None} when instantiated, and will be automatically determined the first time the L{alert} method is called. ''' def __init__(self, name, data_file='data.dpl'): ''' Note - this is a module-B{private} constructor. The method signature for this class may change without notice. ''' self.name = name self.filename = data_file self.connection = None self.logger = None self.default_repeatable_alerts = None def get_data_dir(self, create_dir=True): try: return self.config_dir except AttributeError: config_dir = determine_configuration_directory(subname=self.name, create_dir=create_dir) if create_dir: self.config_dir = config_dir return config_dir def get_data_file_path(self, create_dir=True): return os.path.join(self.get_data_dir(create_dir), self.filename) def load_data(self): data_file_path = self.get_data_file_path() if not os.path.exists(data_file_path): return None data_file = file(data_file_path, 'rb') data = data_file.read() data_file.close() return _zunpickle(data) def save_data(self, data): data_file_path = self.get_data_file_path() data_file = file(data_file_path, 'wb') data_file.write(_zpickle(data)) data_file.close() def get_log_file_path(self, create_dir=True): return os.path.join(self.get_data_dir(create_dir), 'log.txt') def get_log_config_path(self, create_dir=True): return os.path.join(self.get_data_dir(create_dir), 'logconfig.ini') def alert(self, message, alert_type='info', repeatable=None): if self.log_channel is None: return # Azureus 2.4.0.0 and onwards have a Hide All button, therefore we # don't mind having the same message popping up. if repeatable is None: if self.default_repeatable_alerts is None: if self.connection is None: self.default_repeatable_alerts = False else: self.default_repeatable_alerts = \ self.connection.get_azureus_version() >= (2, 4, 0, 0) repeatable = self.default_repeatable_alerts alert_code = { 'warn': self.log_channel.LT_WARNING, 'error': self.log_channel.LT_ERROR, }.get(alert_type, self.log_channel.LT_INFORMATION) if repeatable: _log = self.log_channel.logAlertRepeatable else: _log = self.log_channel.logAlert import dopal.errors try: _log(alert_code, message) except dopal.errors.DopalError: pass def log_channel(self): if hasattr(self, '_log_channel_factory'): return self._log_channel_factory() return None log_channel = _lazyattr(log_channel) def _zunpickle(byte_data): import pickle, zlib return pickle.loads(zlib.decompress(byte_data)) def _zpickle(data_object): import pickle, zlib return zlib.compress(pickle.dumps(data_object)) # # Methods for manipulating the default connection data. # def input_connection_data(): print print 'Enter the default connection data to be used for scripts.' print save_file = save_connection_data(ask_for_connection_data()) print print 'Data saved to', save_file def ask_for_connection_data(): connection_details = {} connection_details['host'] = raw_input('Enter host: ') port_text = raw_input('Enter port (default is 6884): ') if port_text: connection_details['port'] = int(port_text) # Username and password. username = raw_input('Enter user name (leave blank if not applicable): ') password = None if username: import getpass connection_details['user'] = username password1 = getpass.getpass('Enter password: ') password2 = getpass.getpass('Confirm password: ') if password1 != password2: raise ValueError, "Password mismatch!" connection_details['password'] = password1 # Additional information related to the connection. print print 'The following settings are for advanced connection configuration.' print 'Just leave these values blank if you are unsure what to set them to.' print additional_details = {} additional_details['persistent'] = raw_input( "Enable connection persistency [type 'no' to disable]: ") != 'no' timeout_value = raw_input('Set socket timeout (0 to disable, blank to use script default): ') if timeout_value.strip(): additional_details['timeout'] = int(timeout_value.strip()) return connection_details, additional_details def save_connection_data(data_dict): ss = ScriptEnvironment(None, 'connection.dpl') ss.save_data(data_dict) return ss.get_data_file_path() def load_connection_data(error=True): ss = ScriptEnvironment(None, 'connection.dpl') data = ss.load_data() if data is None and error: from dopal.errors import NoDefaultScriptConnectionError raise NoDefaultScriptConnectionError, "No default connection data found - you must run dopal.scripting.input_connection_data(), or if you are running as a script, use the --setup-connection parameter." return data def get_stored_connection(): return _get_connection_from_config(None, None, None, False, False) def _sys_exit(exitcode, message=''): import sys if message: print >> sys.stderr, message sys.exit(exitcode) def _press_any_key_to_exit(): # We use getpass to swallow input, because we don't want to echo # any nonsense that the user types in. print import getpass getpass.getpass("Press any key to exit...") def _configure_logging(script_env, setup_logging): try: import logging except ImportError: return False if setup_logging is False: import dopal.logutils dopal.logutils.noConfig() elif setup_logging is True: logging.basicConfig() else: log_ini = script_env.get_log_config_path(create_dir=False) if not os.path.exists(log_ini): log_ini = ScriptEnvironment(None).get_log_config_path(create_dir=False) if os.path.exists(log_ini): import logging.config logging.config.fileConfig(log_ini) else: import dopal.logutils dopal.logutils.noConfig() return True def _create_handlers(script_env, log_to_file, log_file, log_to_azureus): try: import logging.handlers except ImportError: return [] created_handlers = [] if log_to_file: if log_file is None: log_file = script_env.get_log_path() handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=2000000) created_handlers.append(handler) return created_handlers def _get_remote_logger(script_env, use_own_log_channel): import dopal.errors, types try: logger = script_env.connection.getPluginInterface().getLogger() channel_by_name = dict([(channel.getName(), channel) for channel in logger.getChannels()]) if isinstance(use_own_log_channel, types.StringTypes): log_channel_name = use_own_log_channel elif use_own_log_channel: log_channel_name = name else: log_channel_name = 'DOPAL Scripts' # Reuse an existing channel, or create a new one. if log_channel_name in channel_by_name: return channel_by_name[log_channel_name] else: return logger.getChannel(log_channel_name) except dopal.errors.DopalError, e: # Not too sure about this at the moment. It's probably better to # provide some way to let errors escape. import dopal if dopal.__dopal_mode__ == 1: raise return None def _get_connection_from_config(script_env, connection, timeout, establish_connection, silent_on_connection_error): import dopal.errors if script_env is None: logger = None else: logger = script_env.logger extended_settings = {} if connection is None: if logger: logger.debug("No connection explicitly defined, attempting to load DOPAL scripting default settings.") connection_details, extended_settings = load_connection_data() if logger: logger.debug("Connection settings loaded, about to create connection.") import dopal.main connection = dopal.main.make_connection(**connection_details) if logger: logger.debug("Connection created. Processing advanced settings...") if timeout is not None: timeout_to_use = timeout elif extended_settings.has_key('timeout'): timeout_to_use = extended_settings['timeout'] else: timeout_to_use = None if timeout_to_use is not None: # This is how we distinguish between not giving a value, and turning # timeouts off - 0 means don't use timeouts, and None means "don't do # anything". if timeout_to_use == 0: timeout_to_use = None if logger: logger.debug("Setting timeout to %s." % timeout_to_use) import socket try: socket.setdefaulttimeout(timeout_to_use) except AttributeError: # Not Python 2.2 pass connection.is_persistent_connection = extended_settings.get('persistent', True) if not establish_connection: return connection if logger: logger.debug("About to establish connection to %s." % connection.get_cgi_path(auth_details=True)) try: connection.establish_connection() except dopal.errors.LinkError: if silent_on_connection_error: if logger: logger.info("Failed to establish connection.", exc_info=1) return None else: if logger: logger.exception("Failed to establish connection.") raise else: if logger: logger.debug("Connection established.") return connection def ext_run(name, function, # Connection related. connection=None, make_connection=True, # Connection setup. timeout=15, # Remote logging related. use_repeatable_remote_notification=None, use_own_log_channel=False, remote_notify_on_run=False, remote_notify_on_error=True, # Local logging related. logger=None, setup_logging=None, log_to_file=False, log_level=None, log_file=None, # Exit behaviour. silent_on_connection_error=False, pause_on_exit=0, print_error_on_pause=1): ''' Prepares a L{ScriptEnvironment} object based on the settings here, and executes the passed function. You may alternatively want to use the L{run} function if you don't wish to determine the environment settings to run in, and would prefer the settings to be controlled through arguments on the command line. @note: If passing additional arguments, you must use named arguments, and not rely on the position of the arguments, as these arguments may be moved or even completely removed in later releases. @param name: The I{name} of the script - used for storing data, log files and so on. @param function: The callable object to invoke. Must take one argument, which will be the L{ScriptEnvironment} instance. @param connection: The L{AzureusObjectConnection} object to use - if C{None} is provided, one will be automatically determined for you. @param make_connection: Determines whether the C{scripting} module should attempt to create a connection based on the default connection details or not. Only has an effect if the C{connection} parameter is C{None}. @param timeout: Defines how long socket operations should wait before timing out for (in seconds). Specify C{0} to disable timeouts, the default is C{15}. Specifying C{None} will resort to using the default timeout value specified in the connection details. @param use_repeatable_remote_notification: Determines whether the L{alert} method should use repeatable notification by default or not (see L{ScriptEnvironment.alert}). @param use_own_log_channel: Determines what log channel to use. The default behaviour is to use a log channel called "C{DOPAL Scripts}". Passing a string value will result in logging output being sent to a channel with the given name. Passing C{True} will result in a channel being used which has the same name as the script. @param remote_notify_on_run: Determines whether to send L{alert} calls when the script starts and ends. Normally, this is only desired when testing that the script is working. @param remote_notify_on_error: Determines whether to send an alert to the Azureus connection if an error has occurred during the script's execution. @param logger: The C{logging.Logger} instance to log to - the root logger will be used by default. Will be C{None} if the C{logging} module is not available on the system. @param setup_logging: Determines whether automatically set up logging with the C{logging.Logger} module. If C{True}, C{logging.basicConfig} will be called. If C{False}, L{dopal.logutils.noConfig} will be called. If C{None} (default), then this module will look for file named C{log.ini}, firstly in the script's data directory and then in the global DOPAL scripts directory. If such a file can be found, then C{logging.fileConfig} will be invoked, otherwise L{dopal.logutils.noConfig} will be called instead. @param log_to_file: If C{True}, then a C{RotatingFileHandler} will log to a file in the script's data directory. @param log_level: The logging level assigned to any logger or handlers I{created} by this function. @param log_file: If C{log_to_file} is C{True}, this parameter specifies determines which file to log to (default is that the script will determine a path automatically). @param silent_on_connection_error: If C{True}, this function will silently exit if a connection cannot be established with the stored connection object. Otherwise, the original error will be raised. @param pause_on_exit: If set to C{0} (default), then after execution of the script has occurred, the function will immediately return. If C{1}, the script will wait for keyboard input before terminating. If C{2}, the script will wait for keyboard input only if an error has occurred. @param print_error_on_pause: If C{pause_on_exit} is enabled, this flag determines whether any traceback should be printed. If C{0}, no traceback will be printed. If C{1} (default), any error which occurs inside this function will be printed. If C{2}, only tracebacks which have occurred in the script will be printed. If C{3}, only tracebacks which have occurred outside of the script's invocation will be printed. @raises ScriptFunctionError: Any exception which occurs in the function passed in will be wrapped in this exception. ''' from dopal.errors import raise_as, ScriptFunctionError try: # This will be eventually become a parameter on this method in a later # version of DOPAL, so I'll declare the variable here and program the # code with it in mind. log_to_azureus = False # All data for the script will be stored here. script_env = ScriptEnvironment(name) # First step, initialise the logging environment. # # We do this if we have not been passed a logger object. if logger is None: # We don't call this method if we have been specifically # asked to construct handlers from these function arguments. # # (Currently, that's just "log_to_file" that we want to check.) if log_to_file: logging_configured_by_us = False # We want to log to Azureus, but we can't set that up yet, because # we don't have a connection set up (probably). Adding a logging # handler is the last thing we do before invoking the script, because # we don't want to log any scripting initialisation messages here # remotely (we only want to log what the script wants to log). elif log_to_azureus: logging_configured_by_us = _configure_logging(script_env, False) # Configure using the setup_logging flag. else: logging_configured_by_us = _configure_logging(script_env, setup_logging) if logging_configured_by_us: import logging logger = logging.getLogger() if log_level is not None: logger.setLevel(log_level) else: logging_configured_by_us = False script_env.logger = logger set_levels_on_handlers = \ (log_level is not None) and (not logging_configured_by_us) del logging_configured_by_us # Setup all handlers, apart from any remote handlers... for handler in _create_handlers(script_env, log_to_file, log_file, None): if set_levels_on_handlers: handler.setLevel(log_level) # Next step, sort out a connection (if we need to). if connection is None and make_connection: connection = _get_connection_from_config(script_env, None, timeout, True, silent_on_connection_error) # If connection is None, that means that we failed to establish a # connection, but we don't mind, so just return silently. if connection is None: return # Assign connection if we've got one. if connection is not None: script_env.connection = connection # Next step, setup a remote channel for us to communicate with Azureus. if connection is not None: def make_log_channel(): return _get_remote_logger(script_env, use_own_log_channel) script_env._log_channel_factory = make_log_channel script_env.default_repeatable_alerts = use_repeatable_remote_notification # Configure remote handlers at this point. for handler in _create_handlers(script_env, False, None, log_to_azureus): if set_levels_on_handlers: handler.setLevel(log_level) if remote_notify_on_run: script_env.alert('About to start script "%s"...' % name, repeatable=True) try: function(script_env) except Exception, e: if logger: logger.exception("Error occurred inside script.") # Do we want to notify Azureus? if remote_notify_on_error: script_env.alert( 'An error has occurred while running the script "%s".\nPlease check any related logs - the script\'s data directory is located at:\n %s' % ( script_env.name, script_env.get_data_dir(create_dir=False)), alert_type='error') raise_as(e, ScriptFunctionError) if remote_notify_on_run: script_env.alert('Finished running script "%s".' % name, repeatable=True) # Error during execution. except: if pause_on_exit: # Do we want to log the exception? import sys _exc_type, _exc_value, _exc_tb = sys.exc_info() if isinstance(_exc_value, ScriptFunctionError): _print_tb = print_error_on_pause in [1, 2] # If we are printing the traceback, we do need to print the # underlying traceback if we have a ScriptFunctionError. _exc_value = _exc_value.error _exc_type = _exc_value.__class__ else: _print_tb = print_error_on_pause in [1, 3] if _print_tb: import traceback traceback.print_exception(_exc_type, _exc_value, _exc_tb) _press_any_key_to_exit() # Reraise the original error. raise # Script finished cleanly, just exit normally. else: if pause_on_exit == 1: _press_any_key_to_exit() def run(name, function): ''' Main entry point for script functions to be executed in a preconfigured environment. This function wraps up the majority of the functionality offered by L{ext_run}, except it allows it to be configured through command line arguments. This function requires the C{logging} and C{optparse} (or C{optik}) modules to be present - if they are not (which is the case for a standard Python 2.2 distribution), then a lot of the configurability which is normally provided will not be available. You can find all the configuration options that are available by running this function and passing the C{--help} command line option. There are several options available which will affect how the script is executed, as well as other options which will do something different other than executing the script (such as configuring the default connection). This script can be passed C{None} as the function value - this will force all the command line handling and so on to take place, without requiring a script to be executed. This is useful if you want to know whether calling this function will actually result in your script being executed - for example, you might want to print the text C{"Running script..."}, but only if your script is actually going to executed. This function does not return a value - if this method returns cleanly, then it means the script has been executed (without any problems). This function will raise C{SystemExit} instances if it thinks it is appropriate to do so - this is always done if the script actually fails to be executed. The exit codes are:: 0 - Exit generated by optparse (normally when running with C{--help}). 2 - Required module is missing. 3 - No default connection stored. 4 - Error parsing command line arguments. 5 - Connection not established. 16 - Script not executed (command line options resulted in some other behaviour to occur). If an exception occurs inside the script, it will be passed back to the caller of this function, but it will be wrapped in a L{ScriptFunctionError} instance. If any exception occurs inside the script, in this function, or in L{ext_run}, it will be passed back to the caller of this function (rather than being suppressed). @note: C{sys.excepthook} may be modified by this function to ensure that an exception is only printed once to the user with the most appopriate information. ''' EXIT_TRACEBACK = 1 EXIT_MISSING_MODULE = 2 EXIT_NO_CONNECTION_STORED = 3 EXIT_OPTION_PARSING = 4 EXIT_COULDNT_ESTABLISH_CONNECTION = 5 EXIT_SCRIPT_NOT_EXECUTED = 16 def abort_if_no_connection(): if load_connection_data(error=False) is None: _sys_exit(EXIT_NO_CONNECTION_STORED, "No connection data stored, please re-run with --setup-connection.") try: from optik import OptionGroup, OptionParser, OptionValueError, TitledHelpFormatter except ImportError: try: from optparse import OptionGroup, OptionParser, OptionValueError, TitledHelpFormatter except ImportError: import sys if len(sys.argv) == 1: abort_if_no_connection() if function is not None: ext_run(name, function) return _module_msg = "Cannot run - you either need to:\n" + \ " - Install Python 2.3 or greater\n" + \ " - the 'optik' module from http://optik.sf.net\n" + \ " - Run with no command line arguments." _sys_exit(EXIT_MISSING_MODULE, _module_msg) # Customised help formatter. # # Why do we need one? We don't. # Why do *I* want one? Here's why: # class DOPALCustomHelpFormatter(TitledHelpFormatter): # # 1) Choice options which I create will have a metavar containing # a long string of all the options that can be used. If it's # bunched together with other options, it doesn't read well, so # I want an extra space. # def format_option(self, option): if option.choices is not None: prefix = '\n' else: prefix = '' return prefix + TitledHelpFormatter.format_option(self, option) # # 2) I don't like the all-lower-case "options" header, so we # capitalise it. # def format_heading(self, heading): if heading == 'options': heading = 'Options' return TitledHelpFormatter.format_heading(self, heading) # # 3) I don't like descriptions not being separated out from option # strings, hence the extra space. # def format_description(self, description): result = TitledHelpFormatter.format_description(self, description) if description[-1] == '\n': result += '\n' return result parser = OptionParser(formatter=DOPALCustomHelpFormatter(), usage='%prog [options] [--help]') def parser_error(msg): import sys parser.print_usage(sys.stderr) _sys_exit(EXIT_OPTION_PARSING, msg) parser.error = parser_error # We want to raise a different error code on exit. def add_option(optname, options, help_text, group=None): options_processing = [opt.lower() for opt in options] # This is the rest of the help text we will generate. help_text_additional = ': one of ' + \ ', '.join(['"%s"' % option for option in options]) + '.' if group is not None: parent = group else: parent = parser parent.add_option( '--' + optname, type="choice", metavar='[' + ', '.join(options) + ']', choices=options_processing, dest=optname.replace('-', '_'), help=help_text, # + help_text_additional, ) logging_group = OptionGroup(parser, "Logging setup options", "These options will configure how logging is setup for the script.") parser.add_option_group(logging_group) add_option( 'run-mode', ['background', 'command', 'app'], 'profile to run script in' ) add_option( 'logging', ['none', 'LOCAL'], # , 'remote', 'FULL'], 'details where the script can send log messages to', logging_group, ) add_option( 'loglevel', ['debug', 'info', 'WARN', 'error', 'fatal'], 'set the threshold level for logging', logging_group, ) add_option( 'logdest', ['FILE', 'stderr'], 'set the destination for local logging output', logging_group, ) logging_group.add_option('--logfile', type='string', help='log file to write out to') add_option( 'needs-connection', ['YES', 'no'], 'indicates whether the ability to connect is required, if not, then it causes the script to terminate cleanly', ) add_option( 'announce', ['yes', 'ERROR', 'no'], 'indicates whether the user should be alerted via Azureus when the script starts and stops (or just when errors occur)' ) add_option( 'pause-on-exit', ['yes', 'error', 'NO'], 'indicates whether the script should pause and wait for keyboard input before terminating' ) connection_group = OptionGroup(parser, "Connection setup options", "These options are used to set up and test your own personal " "connection settings. Running with any of these options will cause " "the script not to be executed.\n") connection_group.add_option('--setup-connection', action="store_true", help="Setup up the default connection data for scripts.") connection_group.add_option('--test-connection', action="store_true", help="Test that DOPAL can connect to the connection configured.") connection_group.add_option('--delete-connection', action="store_true", help="Removes the stored connection details.") script_env_group = OptionGroup(parser, "Script setup options", "These options are used to extract and set information related to " "the environment set up for the script. Running with any of these " "options will cause the script not to be executed.\n") script_env_group.add_option('--data-dir-info', action="store_true", help="Prints out where the data directory is for this script.") parser.add_option_group(connection_group) parser.add_option_group(script_env_group) options, args = parser.parse_args() # We don't permit an explicit filename AND a conflicting log destination. if options.logdest not in [None, 'file'] and options.logfile: parser.error("cannot set conflicting --logdest and --logfile values") # We don't allow any command line argument which will make us log to file # if local logging isn't enabled. if options.logging not in [None, 'local', 'full'] and \ (options.logdest or options.logfile or options.loglevel): parser.error("--logging setting conflicts with other parameters") # Want to know where data is kept? if options.data_dir_info: def _process_senv(senv): def _process_senv_file(fpath_func, descr): fpath = fpath_func(create_dir=False) print descr + ':', if not os.path.exists(fpath): print '(does not exist)', print print ' "%s"' % fpath print if senv.name is None: names = [ 'Global data directory', 'Global default connection details', 'Global logging configuration file', ] else: names = [ 'Script data directory', 'Script data file', 'Script logging configuration file', ] _process_senv_file(senv.get_data_dir, names[0]) _process_senv_file(senv.get_data_file_path, names[1]) _process_senv_file(senv.get_log_config_path, names[2]) _process_senv(ScriptEnvironment(None, 'connection.dpl')) _process_senv(ScriptEnvironment(name)) _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) # Delete connection details? if options.delete_connection: conn_path = ScriptEnvironment(None, 'connection.dpl').get_data_file_path(create_dir=False) if not os.path.exists(conn_path): print 'No stored connection data file found.' else: try: os.remove(conn_path) except OSError, error: print 'Unable to delete "%s"...' % conn_path print ' ', error else: print 'Deleted "%s"...' % conn_path _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) # Do we need to setup a connection. if options.setup_connection: input_connection_data() _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) # Want to test the connection? if options.test_connection: abort_if_no_connection() connection = get_stored_connection() print 'Testing connection to', connection.link_data['host'], '...' import dopal.errors try: connection.establish_connection(force=False) except dopal.errors.LinkError, error: print "Unable to establish a connection..." print " Destination:", connection.get_cgi_path(auth_details=True) print " Error:", error.to_error_string() _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) else: print "Connection established, examining XML/HTTP plugin settings..." # While we're at it, let the user know whether their settings are # too restrictive. # # XXX: We need a subclass of RemoteMethodError representing # Access Denied messages. from dopal.errors import NoSuchMethodError, RemoteMethodError # Read-only methods? try: connection.get_plugin_interface().getTorrentManager() except RemoteMethodError: read_only = True else: read_only = False # XXX: Some sort of plugin utility module? if read_only: print print 'NOTE: The XML/HTTP plugin appears to be set to read-only - this may restrict' print ' scripts from working properly.' _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) # Generic classes became the default immediately after 2.4.0.2. if connection.get_azureus_version() > (2, 4, 0, 2): generic_classes = True generic_classes_capable = True elif connection.get_azureus_version() < (2, 4, 0, 0): generic_classes = False generic_classes_capable = False else: generic_classes_capable = True try: connection.get_plugin_interface().getLogger() except NoSuchMethodError: generic_classes = False else: generic_classes = True if not generic_classes: print if generic_classes_capable: print 'NOTE: The XML/HTTP plugin appears to have the "Use generic classes"' print ' setting disabled. This may prevent some scripts from running' print ' properly - please consider enabling this setting.' else: print 'NOTE: This version of Azureus appears to be older than 2.4.0.0.' print ' This may prevent some scripts from running properly.' print ' Please consider upgrading an updated version of Azureus.' else: print 'No problems found with XML/HTTP plugin settings.' _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) # Is the logging module available? try: import logging except ImportError: logging_available = False else: logging_available = True # Now we need to figure out what settings have been defined. # # In level of importance: # - Option on command line. # - Default options for chosen profile. # - Default global settings. # Global default settings. settings = { 'logging': 'none', 'needs_connection': 'yes', 'announce': 'error', 'pause_on_exit': 'no', } # Profile default settings. # # I'll only define those settings which differ from the global defaults. settings.update({ 'background': { 'needs_connection': 'no' }, 'command': { 'logging': 'none', 'announce': 'no', }, 'app': { 'logging': 'none', 'pause_on_exit': 'error', 'announce': 'no', }, None: {}, }[options.run_mode]) # Explicitly given settings. for setting_name in settings.keys(): if getattr(options, setting_name) is not None: settings[setting_name] = getattr(options, setting_name) # Ensure that the user doesn't request logging settings which we can't # support. # # logdest = file or stderr # logfile = blah # logging -> if local, then log to (default) file. if not logging_available and \ (options.loglevel is not None or \ settings['logging'] != 'none' or \ options.logfile or options.logdest): _module_msg = "Cannot run - you either need to:\n" + \ " - Install Python 2.3 or greater\n" + \ " - the 'logging' module from http://www.red-dove.com/python_logging.html\n" + \ " - Run the command again without --loglevel or --logging parameters" _sys_exit(EXIT_MISSING_MODULE, _module_msg) # What log level to use? loglevel = None if options.loglevel is not None: loglevel = getattr(logging, options.loglevel.upper()) # Now we interpret the arguments given and execute ext_run. kwargs = {} kwargs['silent_on_connection_error'] = settings['needs_connection'] == 'no' kwargs['pause_on_exit'] = {'yes': 1, 'no': 0, 'error': 2}[settings['pause_on_exit']] kwargs['remote_notify_on_run'] = settings['announce'] == 'yes' kwargs['remote_notify_on_error'] = settings['announce'] in ['yes', 'error'] # Logging settings. if options.logdest == 'stderr': setup_logging = True logging_to_stderr = True else: setup_logging = None logging_to_stderr = False kwargs['setup_logging'] = setup_logging kwargs['log_level'] = loglevel kwargs['log_to_file'] = options.logdest == 'file' or \ options.logfile is not None kwargs['log_file'] = options.logfile # print_error_on_pause: # Do we want to print the error? That's a bit tough... # # If we know that we are logging to stderr, then any internal script # error will already be printed, so we won't want to do it in that case. # # If an error has occurred while setting up, we will let it be printed # if we pause on errors, but then we have to suppress it from being # reprinted (through sys.excepthook). Otherwise, we can let sys.excepthook # handle it. # # If we aren't logging to stderr, and an internal script error occurs, # we can do the same thing as we currently do for setting up errors. # # However, if we are logging to stderr, we need to remember that setting # up errors aren't fed through to the logger, so we should print setting # up errors. if logging_to_stderr: # Print only initialisation errors. kwargs['print_error_on_pause'] = 3 else: # Print all errors. kwargs['print_error_on_pause'] = 1 print_traceback_in_ext_run = kwargs['pause_on_exit'] and kwargs['print_error_on_pause'] abort_if_no_connection() from dopal.errors import LinkError, ScriptFunctionError # Execute script. if function is not None: try: ext_run(name, function, **kwargs) except LinkError, error: print "Unable to establish a connection..." print " Connection:", error.obj print " Error:", error.to_error_string() _sys_exit(EXIT_SCRIPT_NOT_EXECUTED) except: # Override sys.excepthook here. # # It does two things - firstly, if we know that the traceback # has already been printed to stderr, then we suppress it # being printed again. Secondly, if the exception is a # ScriptFunctionError, it will print the original exception # instead. import sys previous_except_hook = sys.excepthook def scripting_except_hook(exc_type, exc_value, exc_tb): is_script_function_error = False if isinstance(exc_value, ScriptFunctionError): exc_value = exc_value.error exc_type = exc_value.__class__ is_script_function_error = True if logging_to_stderr and is_script_function_error: # Only script function errors will be logged to the # logger, so we'll only suppress the printing of this # exception if the exception is a scripting function # error. return if print_traceback_in_ext_run: return previous_except_hook(exc_type, exc_value, exc_tb) sys.excepthook = scripting_except_hook raise return if __name__ == '__main__': SCRIPT_NAME = 'scripting_main' # Verify that the command line arguments are accepted. run(SCRIPT_NAME, None) # Set up two scripts, one which should work, and the other which will fail. # We add in some delays, just so things don't happen too quickly. print 'The following code will do 2 things - it will run a script which' print 'will work, and then run a script which will fail. This is for' print 'testing purposes.' print def do_something_good(script_env): print "DownloadManager:", script_env.connection.get_plugin_interface().getDownloadManager() def do_something_bad(script_env): print "UploadManager:", script_env.connection.get_plugin_interface().getUploadManager() print 'Running good script...' run(SCRIPT_NAME, do_something_good) print print 'Finished running good script, waiting for 4 seconds...' import time time.sleep(4) print print 'Running bad script...' run(SCRIPT_NAME, do_something_bad) print