plugin.video.torrenter/resources/utorrent/dopal/scripting.py

1450 lines
54 KiB
Python
Raw Permalink Normal View History

2015-01-09 14:11:21 +03:00
# 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<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