| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410 |
- # 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, 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<dopal.objects.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<ScriptEnvironment.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<ScriptEnvironment.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<dopal.errors.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
|