1
0

scripting.py 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. # File: scripting.py
  2. # Library: DOPAL - DO Python Azureus Library
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; version 2 of the License.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details ( see the COPYING file ).
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. '''
  17. This module is designed to provide an 'environment' that allows small scripts
  18. to be written without having to deal with the setting up and exception handling
  19. that you would normally have to deal with.
  20. It also tries to make it straight-forward and distribute scripts without
  21. requiring any modification by another user to get it working on their system
  22. (most common change would be to personalise the script to work with a user's
  23. particular connection setup).
  24. This module provides simple functionality for scripts - data persistency, error
  25. handling and logging - it even provides a mechanism for sending alerts to the
  26. user to be displayed in Azureus (via "Mr Slidey").
  27. There are two main functions provided here:
  28. - C{L{ext_run}} - which provides all the main functionality; and
  29. - C{L{run}} - which calls ext_run with the default settings, but allows these
  30. arguments to be modified through command line arguments.
  31. The following features are provided by this module:
  32. - B{Automatic connection setup} - Default connection settings can be set by
  33. running this module (or any script which uses the run method) with the
  34. C{--setup-connection} command line argument. This will provide the user
  35. with an input prompt to enter connection values, and store it an
  36. appropriate directory (see L{determine_configuration_directory}). That
  37. data is then used for all scripts using that module.
  38. - B{Data Persistency} - You are provided with access to methods to save and
  39. load a pickleable object - the module keeps the data stored in a unique
  40. data directory based on the script's name.
  41. - B{Logging (local)} - A logging system is initialised for the script to log
  42. any messages to - by default, logging to a file in the data directory.
  43. - B{Logging (remote)} - A LoggerChannel is set up to provide the ability to
  44. send log messages to Azureus through it's own logging mechanism. It
  45. also provides the ability to send alerts to the user (via Mr Slidey).
  46. - B{Pause on exit} - The module provides behaviour to pause whenever a script
  47. has finished execution, either in all cases, or only if an error has
  48. occurred. This makes it quite useful if you have the script setup to
  49. run in a window, which closes as soon as the script has terminated.
  50. When writing a script, it should look like this::
  51. def script_function(env):
  52. ... # Do something here.
  53. if __name__ == '__main__':
  54. import dopal.scripting
  55. dopal.scripting.run("functionname", script_function)
  56. where "script_function" is the main body of the script (which takes one
  57. argument, a C{L{ScriptEnvironment}} instance) and "functionname" which is used
  58. to define the script (in terms of where persistent data is sent), and what the
  59. script is called when sending alerts to Azureus.
  60. '''
  61. # Python 2.2 compatibility.
  62. from __future__ import generators
  63. import os, os.path
  64. _default_config_dir = None
  65. def determine_configuration_directory(mainname='DOPAL Scripts', subname=None,
  66. create_dir=True, preserve_case=False):
  67. '''
  68. Determines an appropriate directory to store application data into.
  69. This function will look at environmental settings and registry settings to
  70. determine an appropriate directory.
  71. The locations considered are in order:
  72. - The user's home, as defined by the C{home} environment setting.
  73. - The user's application directory, as determined by the C{win32com} library.
  74. - The user's application directory, as determine by the C{_winreg} library.
  75. - The user's application directory, as defined by the C{appdata} environment setting.
  76. - The user's home, as defined by the C{homepath} environment setting (and if it exists, the C{homedrive} environment setting.
  77. - The user's home, as defined by the C{os.path.expanduser} function.
  78. - The current working directory.
  79. (Note: this order may change between releases.)
  80. If an existing directory can be found, that will be returned. If no
  81. existing directory is found, then this function will try to create the
  82. directory in the most preferred location (based on the order of
  83. preference). If that fails - no existing directory was found and no
  84. directory could be created, then an OSError will be raised. If create_dir
  85. is False and no existing directory can be found, then the most preferred
  86. candidate directory will be returned.
  87. The main argument taken by this function is mainname. This should be a
  88. directory name which is suitable for a Windows application directory
  89. (e.g. "DOPAL Scripts"), as opposed to something which
  90. resembles more Unix-based conventions (e.g. ".dopal_scripts"). This
  91. function will convert the C{mainname} argument into a Unix-style filename
  92. automatically in some cases (read below). You can set the C{preserve_case}
  93. argument to C{True} if you want to prevent automatic name conversation of
  94. this argument to take place.
  95. The C{subname} argument is the subdirectory which gets created in the
  96. main directory. This name will be used literally - no translation of the
  97. directory name will occur.
  98. When this function is considering creating or locating a directory inside
  99. a 'home' location, it will use a Unix-style directory name (e.g.
  100. ".dopal_scripts"). If it is considering an 'application' directory, it will
  101. use a Windows-style directory name (e.g. "DOPAL Scripts"). If it considers
  102. a directory it is unable to categorise (like the current working
  103. directory), it will use a Windows-style name on Windows systems, or a
  104. Unix-style name on all other systems.
  105. @param mainname: The main directory name to store data in - the default is
  106. C{"DOPAL Scripts"}. This value cannot be None.
  107. @param subname: The subdirectory to create in the main directory - this may
  108. be C{None}.
  109. @param create_dir: Boolean value indicating whether we should create the
  110. directory if it doesn't already exist (default is C{True}).
  111. @param preserve_case: Indicates whether the value given in C{mainname}
  112. should be taken literally, or whether name translation can be performed.
  113. Default is C{False}.
  114. @return: A directory which matches the specification given. This directory
  115. is guaranteed to exist, unless this function was called with
  116. C{create_dir} being False.
  117. @raise OSError: If C{create_dir} is C{True}, and no appropriate directory
  118. could be created.
  119. '''
  120. # If we have an application data directory, then we will prefer to use
  121. # that. We will actually iterate over all directories that we consider, and
  122. # return the first directory we find. If we don't manage that, we'll create
  123. # one in the most appropriate directory. We'll also try to stick to some
  124. # naming conventions - using a dot-prefix for home directories, using
  125. # normal looking names in application data directories.
  126. #
  127. # Code is based on a mixture of user.py and homedirectory.py from the
  128. # pyopengl library.
  129. # Our preferred behaviour - existance of a home directory, and creating a
  130. # .dopal_scripts directory there.
  131. if not preserve_case:
  132. app_data_name = mainname
  133. home_data_name = '.' + mainname.lower().replace(' ', '_')
  134. import sys
  135. if sys.platform == 'win32':
  136. unknown_loc_name = app_data_name
  137. else:
  138. unknown_loc_name = home_data_name
  139. else:
  140. app_data_name = home_data_name = unknown_loc_name = mainname
  141. if subname:
  142. app_data_name = os.path.join(app_data_name, subname)
  143. home_data_name = os.path.join(home_data_name, subname)
  144. unknown_loc_name = os.path.join(unknown_loc_name, subname)
  145. def suggested_location():
  146. # 1) Test for the home directory.
  147. if os.environ.has_key('home'):
  148. yield os.environ['home'], home_data_name
  149. # 2) Test for application data - using win32com library.
  150. try:
  151. from win32com.shell import shell, shellcon
  152. yield shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0), app_data_name
  153. except Exception, e:
  154. pass
  155. # 3) Test for application data - using _winreg.
  156. try:
  157. import _winreg
  158. key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
  159. path = _winreg.QueryValueEx(key, 'AppData')[0]
  160. _winreg.CloseKey(key)
  161. yield path, app_data_name
  162. except Exception, e:
  163. pass
  164. # 4) Test for application data - using environment settings.
  165. if os.environ.has_key('appdata'):
  166. yield os.environ['appdata'], app_data_name
  167. # 5) Test for home directory, using other environment settings.
  168. if os.environ.has_key('homepath'):
  169. if os.environ.has_key('homedrive'):
  170. yield os.path.join(os.environ['homedrive'], os.environ['homepath']), home_data_name
  171. else:
  172. yield os.environ['homepath'], home_data_name
  173. # 6) Test for home directory, using expanduser.
  174. expanded_path = os.path.expanduser('~')
  175. if expanded_path != '~':
  176. yield expanded_path, home_data_name
  177. # 7) Try the current directory then.
  178. yield os.getcwd(), unknown_loc_name
  179. # This will go through each option and choose what directory to choose.
  180. # It will keep yielding suggestions until we've decided what we want to
  181. # use.
  182. suggested_unmade_paths = []
  183. for suggested_path, suggested_name in suggested_location():
  184. full_suggested_path = os.path.join(suggested_path, suggested_name)
  185. if os.path.isdir(full_suggested_path):
  186. return full_suggested_path
  187. suggested_unmade_paths.append(full_suggested_path)
  188. # Return the first path we're able to create.
  189. for path in suggested_unmade_paths:
  190. # If we don't want to create a directory, just return the first path
  191. # we have dealt with.
  192. try:
  193. os.makedirs(path)
  194. except OSError, e:
  195. pass
  196. else:
  197. # Success!
  198. if os.path.isdir(path):
  199. return path
  200. # If we get here, then there's nothing we can do. We gave it our best shot.
  201. raise OSError, "unable to create an appropriate directory"
  202. # Lazily-generated attribute stuff for ScriptEnvironment, taken from here:
  203. # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/363602
  204. class _lazyattr(object):
  205. def __init__(self, calculate_function):
  206. self._calculate = calculate_function
  207. def __get__(self, obj, typeobj=None):
  208. if obj is None:
  209. return self
  210. value = self._calculate(obj)
  211. setattr(obj, self._calculate.func_name, value)
  212. return value
  213. #
  214. # Methods used for saving and loading data.
  215. #
  216. class ScriptEnvironment(object):
  217. '''
  218. The ScriptEnvironment class contains values and methods useful for a script
  219. to work with.
  220. @ivar name: The name of the script.
  221. @ivar filename: The filename (no directory information included) of where
  222. persistent data for this object should be stored - default is
  223. C{data.dpl}. If you want to set a different filename, this value should
  224. be set before any saving or loading of persistent data takes place in
  225. the script.
  226. @ivar connection: The AzureusObjectConnection to work with. The connection
  227. should already be in an established state. connection may be None if
  228. ext_run is configured that way.
  229. @ivar logger: The logger instance to log data to. May be C{None}.
  230. @ivar log_channel: The logger channel object to send messages to. May be
  231. C{None}. For convenience the L{alert} method is available on
  232. ScriptEnvironment messages.
  233. @ivar default_repeatable_alerts: Indicates whether alerts are repeatable
  234. or not by default. This can be set explicitly on the object, but it
  235. can also be overridden when calling the L{alert} method. In most cases,
  236. this value will be C{None} when instantiated, and will be automatically
  237. determined the first time the L{alert} method is called.
  238. '''
  239. def __init__(self, name, data_file='data.dpl'):
  240. '''
  241. Note - this is a module-B{private} constructor. The method signature
  242. for this class may change without notice.
  243. '''
  244. self.name = name
  245. self.filename = data_file
  246. self.connection = None
  247. self.logger = None
  248. self.default_repeatable_alerts = None
  249. def get_data_dir(self, create_dir=True):
  250. try:
  251. return self.config_dir
  252. except AttributeError:
  253. config_dir = determine_configuration_directory(subname=self.name, create_dir=create_dir)
  254. if create_dir:
  255. self.config_dir = config_dir
  256. return config_dir
  257. def get_data_file_path(self, create_dir=True):
  258. return os.path.join(self.get_data_dir(create_dir), self.filename)
  259. def load_data(self):
  260. data_file_path = self.get_data_file_path()
  261. if not os.path.exists(data_file_path):
  262. return None
  263. data_file = file(data_file_path, 'rb')
  264. data = data_file.read()
  265. data_file.close()
  266. return _zunpickle(data)
  267. def save_data(self, data):
  268. data_file_path = self.get_data_file_path()
  269. data_file = file(data_file_path, 'wb')
  270. data_file.write(_zpickle(data))
  271. data_file.close()
  272. def get_log_file_path(self, create_dir=True):
  273. return os.path.join(self.get_data_dir(create_dir), 'log.txt')
  274. def get_log_config_path(self, create_dir=True):
  275. return os.path.join(self.get_data_dir(create_dir), 'logconfig.ini')
  276. def alert(self, message, alert_type='info', repeatable=None):
  277. if self.log_channel is None:
  278. return
  279. # Azureus 2.4.0.0 and onwards have a Hide All button, therefore we
  280. # don't mind having the same message popping up.
  281. if repeatable is None:
  282. if self.default_repeatable_alerts is None:
  283. if self.connection is None:
  284. self.default_repeatable_alerts = False
  285. else:
  286. self.default_repeatable_alerts = \
  287. self.connection.get_azureus_version() >= (2, 4, 0, 0)
  288. repeatable = self.default_repeatable_alerts
  289. alert_code = {
  290. 'warn': self.log_channel.LT_WARNING,
  291. 'error': self.log_channel.LT_ERROR,
  292. }.get(alert_type, self.log_channel.LT_INFORMATION)
  293. if repeatable:
  294. _log = self.log_channel.logAlertRepeatable
  295. else:
  296. _log = self.log_channel.logAlert
  297. import dopal.errors
  298. try:
  299. _log(alert_code, message)
  300. except dopal.errors.DopalError:
  301. pass
  302. def log_channel(self):
  303. if hasattr(self, '_log_channel_factory'):
  304. return self._log_channel_factory()
  305. return None
  306. log_channel = _lazyattr(log_channel)
  307. def _zunpickle(byte_data):
  308. import pickle, zlib
  309. return pickle.loads(zlib.decompress(byte_data))
  310. def _zpickle(data_object):
  311. import pickle, zlib
  312. return zlib.compress(pickle.dumps(data_object))
  313. #
  314. # Methods for manipulating the default connection data.
  315. #
  316. def input_connection_data():
  317. print
  318. print 'Enter the default connection data to be used for scripts.'
  319. print
  320. save_file = save_connection_data(ask_for_connection_data())
  321. print
  322. print 'Data saved to', save_file
  323. def ask_for_connection_data():
  324. connection_details = {}
  325. connection_details['host'] = raw_input('Enter host: ')
  326. port_text = raw_input('Enter port (default is 6884): ')
  327. if port_text:
  328. connection_details['port'] = int(port_text)
  329. # Username and password.
  330. username = raw_input('Enter user name (leave blank if not applicable): ')
  331. password = None
  332. if username:
  333. import getpass
  334. connection_details['user'] = username
  335. password1 = getpass.getpass('Enter password: ')
  336. password2 = getpass.getpass('Confirm password: ')
  337. if password1 != password2:
  338. raise ValueError, "Password mismatch!"
  339. connection_details['password'] = password1
  340. # Additional information related to the connection.
  341. print
  342. print 'The following settings are for advanced connection configuration.'
  343. print 'Just leave these values blank if you are unsure what to set them to.'
  344. print
  345. additional_details = {}
  346. additional_details['persistent'] = raw_input(
  347. "Enable connection persistency [type 'no' to disable]: ") != 'no'
  348. timeout_value = raw_input('Set socket timeout (0 to disable, blank to use script default): ')
  349. if timeout_value.strip():
  350. additional_details['timeout'] = int(timeout_value.strip())
  351. return connection_details, additional_details
  352. def save_connection_data(data_dict):
  353. ss = ScriptEnvironment(None, 'connection.dpl')
  354. ss.save_data(data_dict)
  355. return ss.get_data_file_path()
  356. def load_connection_data(error=True):
  357. ss = ScriptEnvironment(None, 'connection.dpl')
  358. data = ss.load_data()
  359. if data is None and error:
  360. from dopal.errors import NoDefaultScriptConnectionError
  361. 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."
  362. return data
  363. def get_stored_connection():
  364. return _get_connection_from_config(None, None, None, False, False)
  365. def _sys_exit(exitcode, message=''):
  366. import sys
  367. if message:
  368. print >>sys.stderr, message
  369. sys.exit(exitcode)
  370. def _press_any_key_to_exit():
  371. # We use getpass to swallow input, because we don't want to echo
  372. # any nonsense that the user types in.
  373. print
  374. import getpass
  375. getpass.getpass("Press any key to exit...")
  376. def _configure_logging(script_env, setup_logging):
  377. try:
  378. import logging
  379. except ImportError:
  380. return False
  381. if setup_logging is False:
  382. import dopal.logutils
  383. dopal.logutils.noConfig()
  384. elif setup_logging is True:
  385. logging.basicConfig()
  386. else:
  387. log_ini = script_env.get_log_config_path(create_dir=False)
  388. if not os.path.exists(log_ini):
  389. log_ini = ScriptEnvironment(None).get_log_config_path(create_dir=False)
  390. if os.path.exists(log_ini):
  391. import logging.config
  392. logging.config.fileConfig(log_ini)
  393. else:
  394. import dopal.logutils
  395. dopal.logutils.noConfig()
  396. return True
  397. def _create_handlers(script_env, log_to_file, log_file, log_to_azureus):
  398. try:
  399. import logging.handlers
  400. except ImportError:
  401. return []
  402. created_handlers = []
  403. if log_to_file:
  404. if log_file is None:
  405. log_file = script_env.get_log_path()
  406. handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=2000000)
  407. created_handlers.append(handler)
  408. return created_handlers
  409. def _get_remote_logger(script_env, use_own_log_channel):
  410. import dopal.errors, types
  411. try:
  412. logger = script_env.connection.getPluginInterface().getLogger()
  413. channel_by_name = dict([(channel.getName(), channel) for channel in logger.getChannels()])
  414. if isinstance(use_own_log_channel, types.StringTypes):
  415. log_channel_name = use_own_log_channel
  416. elif use_own_log_channel:
  417. log_channel_name = name
  418. else:
  419. log_channel_name = 'DOPAL Scripts'
  420. # Reuse an existing channel, or create a new one.
  421. if log_channel_name in channel_by_name:
  422. return channel_by_name[log_channel_name]
  423. else:
  424. return logger.getChannel(log_channel_name)
  425. except dopal.errors.DopalError, e:
  426. # Not too sure about this at the moment. It's probably better to
  427. # provide some way to let errors escape.
  428. import dopal
  429. if dopal.__dopal_mode__ == 1:
  430. raise
  431. return None
  432. def _get_connection_from_config(script_env, connection, timeout, establish_connection, silent_on_connection_error):
  433. import dopal.errors
  434. if script_env is None:
  435. logger = None
  436. else:
  437. logger = script_env.logger
  438. extended_settings = {}
  439. if connection is None:
  440. if logger:
  441. logger.debug("No connection explicitly defined, attempting to load DOPAL scripting default settings.")
  442. connection_details, extended_settings = load_connection_data()
  443. if logger:
  444. logger.debug("Connection settings loaded, about to create connection.")
  445. import dopal.main
  446. connection = dopal.main.make_connection(**connection_details)
  447. if logger:
  448. logger.debug("Connection created. Processing advanced settings...")
  449. if timeout is not None:
  450. timeout_to_use = timeout
  451. elif extended_settings.has_key('timeout'):
  452. timeout_to_use = extended_settings['timeout']
  453. else:
  454. timeout_to_use = None
  455. if timeout_to_use is not None:
  456. # This is how we distinguish between not giving a value, and turning
  457. # timeouts off - 0 means don't use timeouts, and None means "don't do
  458. # anything".
  459. if timeout_to_use == 0:
  460. timeout_to_use = None
  461. if logger:
  462. logger.debug("Setting timeout to %s." % timeout_to_use)
  463. import socket
  464. try:
  465. socket.setdefaulttimeout(timeout_to_use)
  466. except AttributeError: # Not Python 2.2
  467. pass
  468. connection.is_persistent_connection = extended_settings.get('persistent', True)
  469. if not establish_connection:
  470. return connection
  471. if logger:
  472. logger.debug("About to establish connection to %s." % connection.get_cgi_path(auth_details=True))
  473. try:
  474. connection.establish_connection()
  475. except dopal.errors.LinkError:
  476. if silent_on_connection_error:
  477. if logger:
  478. logger.info("Failed to establish connection.", exc_info=1)
  479. return None
  480. else:
  481. if logger:
  482. logger.exception("Failed to establish connection.")
  483. raise
  484. else:
  485. if logger:
  486. logger.debug("Connection established.")
  487. return connection
  488. def ext_run(name, function,
  489. # Connection related.
  490. connection=None, make_connection=True,
  491. # Connection setup.
  492. timeout=15,
  493. # Remote logging related.
  494. use_repeatable_remote_notification=None, use_own_log_channel=False,
  495. remote_notify_on_run=False, remote_notify_on_error=True,
  496. # Local logging related.
  497. logger=None, setup_logging=None, log_to_file=False, log_level=None,
  498. log_file=None,
  499. # Exit behaviour.
  500. silent_on_connection_error=False, pause_on_exit=0, print_error_on_pause=1):
  501. '''
  502. Prepares a L{ScriptEnvironment} object based on the settings here, and
  503. executes the passed function.
  504. You may alternatively want to use the L{run} function if you don't wish
  505. to determine the environment settings to run in, and would prefer the
  506. settings to be controlled through arguments on the command line.
  507. @note: If passing additional arguments, you must use named arguments,
  508. and not rely on the position of the arguments, as these arguments may
  509. be moved or even completely removed in later releases.
  510. @param name: The I{name} of the script - used for storing data, log files
  511. and so on.
  512. @param function: The callable object to invoke. Must take one argument,
  513. which will be the L{ScriptEnvironment} instance.
  514. @param connection: The
  515. L{AzureusObjectConnection<dopal.objects.AzureusObjectConnection>} object
  516. to use - if C{None} is provided, one will be automatically determined
  517. for you.
  518. @param make_connection: Determines whether the C{scripting} module
  519. should attempt to create a connection based on the default connection
  520. details or not. Only has an effect if the C{connection} parameter is
  521. C{None}.
  522. @param timeout: Defines how long socket operations should wait before
  523. timing out for (in seconds). Specify C{0} to disable timeouts, the
  524. default is C{15}. Specifying C{None} will resort to using the default
  525. timeout value specified in the connection details.
  526. @param use_repeatable_remote_notification: Determines whether the
  527. L{alert<ScriptEnvironment.alert>} method should use repeatable
  528. notification by default or not (see L{ScriptEnvironment.alert}).
  529. @param use_own_log_channel: Determines what log channel to use. The default
  530. behaviour is to use a log channel called "C{DOPAL Scripts}". Passing a
  531. string value will result in logging output being sent to a channel with
  532. the given name. Passing C{True} will result in a channel being used
  533. which has the same name as the script.
  534. @param remote_notify_on_run: Determines whether to send
  535. L{alert<ScriptEnvironment.alert>} calls when the script starts and ends.
  536. Normally, this is only desired when testing that the script is working.
  537. @param remote_notify_on_error: Determines whether to send an alert to the
  538. Azureus connection if an error has occurred during the script's
  539. execution.
  540. @param logger: The C{logging.Logger} instance to log to - the root logger
  541. will be used by default. Will be C{None} if the C{logging} module is not
  542. available on the system.
  543. @param setup_logging: Determines whether automatically set up logging with
  544. the C{logging.Logger} module. If C{True}, C{logging.basicConfig} will be
  545. called. If C{False}, L{dopal.logutils.noConfig} will be called. If
  546. C{None} (default), then this module will look for file named C{log.ini},
  547. firstly in the script's data directory and then in the global DOPAL
  548. scripts directory. If such a file can be found, then
  549. C{logging.fileConfig} will be invoked, otherwise
  550. L{dopal.logutils.noConfig} will be called instead.
  551. @param log_to_file: If C{True}, then a C{RotatingFileHandler} will log to a
  552. file in the script's data directory.
  553. @param log_level: The logging level assigned to any logger or handlers
  554. I{created} by this function.
  555. @param log_file: If C{log_to_file} is C{True}, this parameter
  556. specifies determines which file to log to (default is that the script
  557. will determine a path automatically).
  558. @param silent_on_connection_error: If C{True}, this function will silently
  559. exit if a connection cannot be established with the stored connection
  560. object. Otherwise, the original error will be raised.
  561. @param pause_on_exit: If set to C{0} (default), then after execution of the
  562. script has occurred, the function will immediately return. If C{1}, the
  563. script will wait for keyboard input before terminating. If C{2}, the
  564. script will wait for keyboard input only if an error has occurred.
  565. @param print_error_on_pause: If C{pause_on_exit} is enabled, this flag
  566. determines whether any traceback should be printed. If C{0}, no
  567. traceback will be printed. If C{1} (default), any error which occurs
  568. inside this function will be printed. If C{2}, only tracebacks which have
  569. occurred in the script will be printed. If C{3}, only tracebacks which
  570. have occurred outside of the script's invocation will be printed.
  571. @raises ScriptFunctionError: Any exception which occurs in the
  572. function passed in will be wrapped in this exception.
  573. '''
  574. from dopal.errors import raise_as, ScriptFunctionError
  575. try:
  576. # This will be eventually become a parameter on this method in a later
  577. # version of DOPAL, so I'll declare the variable here and program the
  578. # code with it in mind.
  579. log_to_azureus = False
  580. # All data for the script will be stored here.
  581. script_env = ScriptEnvironment(name)
  582. # First step, initialise the logging environment.
  583. #
  584. # We do this if we have not been passed a logger object.
  585. if logger is None:
  586. # We don't call this method if we have been specifically
  587. # asked to construct handlers from these function arguments.
  588. #
  589. # (Currently, that's just "log_to_file" that we want to check.)
  590. if log_to_file:
  591. logging_configured_by_us = False
  592. # We want to log to Azureus, but we can't set that up yet, because
  593. # we don't have a connection set up (probably). Adding a logging
  594. # handler is the last thing we do before invoking the script, because
  595. # we don't want to log any scripting initialisation messages here
  596. # remotely (we only want to log what the script wants to log).
  597. elif log_to_azureus:
  598. logging_configured_by_us = _configure_logging(script_env, False)
  599. # Configure using the setup_logging flag.
  600. else:
  601. logging_configured_by_us = _configure_logging(script_env, setup_logging)
  602. if logging_configured_by_us:
  603. import logging
  604. logger = logging.getLogger()
  605. if log_level is not None:
  606. logger.setLevel(log_level)
  607. else:
  608. logging_configured_by_us = False
  609. script_env.logger = logger
  610. set_levels_on_handlers = \
  611. (log_level is not None) and (not logging_configured_by_us)
  612. del logging_configured_by_us
  613. # Setup all handlers, apart from any remote handlers...
  614. for handler in _create_handlers(script_env, log_to_file, log_file, None):
  615. if set_levels_on_handlers:
  616. handler.setLevel(log_level)
  617. # Next step, sort out a connection (if we need to).
  618. if connection is None and make_connection:
  619. connection = _get_connection_from_config(script_env, None, timeout, True, silent_on_connection_error)
  620. # If connection is None, that means that we failed to establish a
  621. # connection, but we don't mind, so just return silently.
  622. if connection is None:
  623. return
  624. # Assign connection if we've got one.
  625. if connection is not None:
  626. script_env.connection = connection
  627. # Next step, setup a remote channel for us to communicate with Azureus.
  628. if connection is not None:
  629. def make_log_channel():
  630. return _get_remote_logger(script_env, use_own_log_channel)
  631. script_env._log_channel_factory = make_log_channel
  632. script_env.default_repeatable_alerts = use_repeatable_remote_notification
  633. # Configure remote handlers at this point.
  634. for handler in _create_handlers(script_env, False, None, log_to_azureus):
  635. if set_levels_on_handlers:
  636. handler.setLevel(log_level)
  637. if remote_notify_on_run:
  638. script_env.alert('About to start script "%s"...' % name, repeatable=True)
  639. try:
  640. function(script_env)
  641. except Exception, e:
  642. if logger:
  643. logger.exception("Error occurred inside script.")
  644. # Do we want to notify Azureus?
  645. if remote_notify_on_error:
  646. 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')
  647. raise_as(e, ScriptFunctionError)
  648. if remote_notify_on_run:
  649. script_env.alert('Finished running script "%s".' % name, repeatable=True)
  650. # Error during execution.
  651. except:
  652. if pause_on_exit:
  653. # Do we want to log the exception?
  654. import sys
  655. _exc_type, _exc_value, _exc_tb = sys.exc_info()
  656. if isinstance(_exc_value, ScriptFunctionError):
  657. _print_tb = print_error_on_pause in [1, 2]
  658. # If we are printing the traceback, we do need to print the
  659. # underlying traceback if we have a ScriptFunctionError.
  660. _exc_value = _exc_value.error
  661. _exc_type = _exc_value.__class__
  662. else:
  663. _print_tb = print_error_on_pause in [1, 3]
  664. if _print_tb:
  665. import traceback
  666. traceback.print_exception(_exc_type, _exc_value, _exc_tb)
  667. _press_any_key_to_exit()
  668. # Reraise the original error.
  669. raise
  670. # Script finished cleanly, just exit normally.
  671. else:
  672. if pause_on_exit == 1:
  673. _press_any_key_to_exit()
  674. def run(name, function):
  675. '''
  676. Main entry point for script functions to be executed in a preconfigured
  677. environment.
  678. This function wraps up the majority of the functionality offered by
  679. L{ext_run}, except it allows it to be configured through command line
  680. arguments.
  681. This function requires the C{logging} and C{optparse} (or C{optik}) modules
  682. to be present - if they are not (which is the case for a standard Python
  683. 2.2 distribution), then a lot of the configurability which is normally
  684. provided will not be available.
  685. You can find all the configuration options that are available by running
  686. this function and passing the C{--help} command line option.
  687. There are several options available which will affect how the script is
  688. executed, as well as other options which will do something different other
  689. than executing the script (such as configuring the default connection).
  690. This script can be passed C{None} as the function value - this will force
  691. all the command line handling and so on to take place, without requiring
  692. a script to be executed. This is useful if you want to know whether
  693. calling this function will actually result in your script being executed -
  694. for example, you might want to print the text C{"Running script..."}, but
  695. only if your script is actually going to executed.
  696. This function does not return a value - if this method returns cleanly,
  697. then it means the script has been executed (without any problems). This
  698. function will raise C{SystemExit} instances if it thinks it is appropriate
  699. to do so - this is always done if the script actually fails to be executed.
  700. The exit codes are::
  701. 0 - Exit generated by optparse (normally when running with C{--help}).
  702. 2 - Required module is missing.
  703. 3 - No default connection stored.
  704. 4 - Error parsing command line arguments.
  705. 5 - Connection not established.
  706. 16 - Script not executed (command line options resulted in some other behaviour to occur).
  707. If an exception occurs inside the script, it will be passed back to the
  708. caller of this function, but it will be wrapped in a
  709. L{ScriptFunctionError<dopal.errors.ScriptFunctionError>} instance.
  710. If any exception occurs inside the script, in this function, or in
  711. L{ext_run}, it will be passed back to the caller of this function (rather
  712. than being suppressed).
  713. @note: C{sys.excepthook} may be modified by this function to ensure that
  714. an exception is only printed once to the user with the most appopriate
  715. information.
  716. '''
  717. EXIT_TRACEBACK = 1
  718. EXIT_MISSING_MODULE = 2
  719. EXIT_NO_CONNECTION_STORED = 3
  720. EXIT_OPTION_PARSING = 4
  721. EXIT_COULDNT_ESTABLISH_CONNECTION = 5
  722. EXIT_SCRIPT_NOT_EXECUTED = 16
  723. def abort_if_no_connection():
  724. if load_connection_data(error=False) is None:
  725. _sys_exit(EXIT_NO_CONNECTION_STORED,
  726. "No connection data stored, please re-run with --setup-connection.")
  727. try:
  728. from optik import OptionGroup, OptionParser, OptionValueError, TitledHelpFormatter
  729. except ImportError:
  730. try:
  731. from optparse import OptionGroup, OptionParser, OptionValueError, TitledHelpFormatter
  732. except ImportError:
  733. import sys
  734. if len(sys.argv) == 1:
  735. abort_if_no_connection()
  736. if function is not None:
  737. ext_run(name, function)
  738. return
  739. _module_msg = "Cannot run - you either need to:\n" + \
  740. " - Install Python 2.3 or greater\n" + \
  741. " - the 'optik' module from http://optik.sf.net\n" + \
  742. " - Run with no command line arguments."
  743. _sys_exit(EXIT_MISSING_MODULE, _module_msg)
  744. # Customised help formatter.
  745. #
  746. # Why do we need one? We don't.
  747. # Why do *I* want one? Here's why:
  748. #
  749. class DOPALCustomHelpFormatter(TitledHelpFormatter):
  750. #
  751. # 1) Choice options which I create will have a metavar containing
  752. # a long string of all the options that can be used. If it's
  753. # bunched together with other options, it doesn't read well, so
  754. # I want an extra space.
  755. #
  756. def format_option(self, option):
  757. if option.choices is not None:
  758. prefix = '\n'
  759. else:
  760. prefix = ''
  761. return prefix + TitledHelpFormatter.format_option(self, option)
  762. #
  763. # 2) I don't like the all-lower-case "options" header, so we
  764. # capitalise it.
  765. #
  766. def format_heading(self, heading):
  767. if heading == 'options':
  768. heading = 'Options'
  769. return TitledHelpFormatter.format_heading(self, heading)
  770. #
  771. # 3) I don't like descriptions not being separated out from option
  772. # strings, hence the extra space.
  773. #
  774. def format_description (self, description):
  775. result = TitledHelpFormatter.format_description(self, description)
  776. if description[-1] == '\n':
  777. result += '\n'
  778. return result
  779. parser = OptionParser(formatter=DOPALCustomHelpFormatter(), usage='%prog [options] [--help]')
  780. def parser_error(msg):
  781. import sys
  782. parser.print_usage(sys.stderr)
  783. _sys_exit(EXIT_OPTION_PARSING, msg)
  784. parser.error = parser_error
  785. # We want to raise a different error code on exit.
  786. def add_option(optname, options, help_text, group=None):
  787. options_processing = [opt.lower() for opt in options]
  788. # This is the rest of the help text we will generate.
  789. help_text_additional = ': one of ' + \
  790. ', '.join(['"%s"' % option for option in options]) + '.'
  791. if group is not None:
  792. parent = group
  793. else:
  794. parent = parser
  795. parent.add_option(
  796. '--' + optname,
  797. type="choice",
  798. metavar='[' + ', '.join(options) + ']',
  799. choices=options_processing,
  800. dest=optname.replace('-', '_'),
  801. help=help_text,# + help_text_additional,
  802. )
  803. logging_group = OptionGroup(parser, "Logging setup options",
  804. "These options will configure how logging is setup for the script.")
  805. parser.add_option_group(logging_group)
  806. add_option(
  807. 'run-mode',
  808. ['background', 'command', 'app'],
  809. 'profile to run script in'
  810. )
  811. add_option(
  812. 'logging',
  813. ['none', 'LOCAL'], # , 'remote', 'FULL'],
  814. 'details where the script can send log messages to',
  815. logging_group,
  816. )
  817. add_option(
  818. 'loglevel',
  819. ['debug', 'info', 'WARN', 'error', 'fatal'],
  820. 'set the threshold level for logging',
  821. logging_group,
  822. )
  823. add_option(
  824. 'logdest',
  825. ['FILE', 'stderr'],
  826. 'set the destination for local logging output',
  827. logging_group,
  828. )
  829. logging_group.add_option('--logfile', type='string', help='log file to write out to')
  830. add_option(
  831. 'needs-connection',
  832. ['YES', 'no'],
  833. 'indicates whether the ability to connect is required, if not, then it causes the script to terminate cleanly',
  834. )
  835. add_option(
  836. 'announce',
  837. ['yes', 'ERROR', 'no'],
  838. 'indicates whether the user should be alerted via Azureus when the script starts and stops (or just when errors occur)'
  839. )
  840. add_option(
  841. 'pause-on-exit',
  842. ['yes', 'error', 'NO'],
  843. 'indicates whether the script should pause and wait for keyboard input before terminating'
  844. )
  845. connection_group = OptionGroup(parser, "Connection setup options",
  846. "These options are used to set up and test your own personal "
  847. "connection settings. Running with any of these options will cause "
  848. "the script not to be executed.\n")
  849. connection_group.add_option('--setup-connection', action="store_true",
  850. help="Setup up the default connection data for scripts.")
  851. connection_group.add_option('--test-connection', action="store_true",
  852. help="Test that DOPAL can connect to the connection configured.")
  853. connection_group.add_option('--delete-connection', action="store_true",
  854. help="Removes the stored connection details.")
  855. script_env_group = OptionGroup(parser, "Script setup options",
  856. "These options are used to extract and set information related to "
  857. "the environment set up for the script. Running with any of these "
  858. "options will cause the script not to be executed.\n")
  859. script_env_group.add_option('--data-dir-info', action="store_true",
  860. help="Prints out where the data directory is for this script.")
  861. parser.add_option_group(connection_group)
  862. parser.add_option_group(script_env_group)
  863. options, args = parser.parse_args()
  864. # We don't permit an explicit filename AND a conflicting log destination.
  865. if options.logdest not in [None, 'file'] and options.logfile:
  866. parser.error("cannot set conflicting --logdest and --logfile values")
  867. # We don't allow any command line argument which will make us log to file
  868. # if local logging isn't enabled.
  869. if options.logging not in [None, 'local', 'full'] and \
  870. (options.logdest or options.logfile or options.loglevel):
  871. parser.error("--logging setting conflicts with other parameters")
  872. # Want to know where data is kept?
  873. if options.data_dir_info:
  874. def _process_senv(senv):
  875. def _process_senv_file(fpath_func, descr):
  876. fpath = fpath_func(create_dir=False)
  877. print descr + ':',
  878. if not os.path.exists(fpath):
  879. print '(does not exist)',
  880. print
  881. print ' "%s"' % fpath
  882. print
  883. if senv.name is None:
  884. names = [
  885. 'Global data directory',
  886. 'Global default connection details',
  887. 'Global logging configuration file',
  888. ]
  889. else:
  890. names = [
  891. 'Script data directory',
  892. 'Script data file',
  893. 'Script logging configuration file',
  894. ]
  895. _process_senv_file(senv.get_data_dir, names[0])
  896. _process_senv_file(senv.get_data_file_path, names[1])
  897. _process_senv_file(senv.get_log_config_path, names[2])
  898. _process_senv(ScriptEnvironment(None, 'connection.dpl'))
  899. _process_senv(ScriptEnvironment(name))
  900. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  901. # Delete connection details?
  902. if options.delete_connection:
  903. conn_path = ScriptEnvironment(None, 'connection.dpl').get_data_file_path(create_dir=False)
  904. if not os.path.exists(conn_path):
  905. print 'No stored connection data file found.'
  906. else:
  907. try:
  908. os.remove(conn_path)
  909. except OSError, error:
  910. print 'Unable to delete "%s"...' % conn_path
  911. print ' ', error
  912. else:
  913. print 'Deleted "%s"...' % conn_path
  914. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  915. # Do we need to setup a connection.
  916. if options.setup_connection:
  917. input_connection_data()
  918. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  919. # Want to test the connection?
  920. if options.test_connection:
  921. abort_if_no_connection()
  922. connection = get_stored_connection()
  923. print 'Testing connection to', connection.link_data['host'], '...'
  924. import dopal.errors
  925. try:
  926. connection.establish_connection(force=False)
  927. except dopal.errors.LinkError, error:
  928. print "Unable to establish a connection..."
  929. print " Destination:", connection.get_cgi_path(auth_details=True)
  930. print " Error:", error.to_error_string()
  931. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  932. else:
  933. print "Connection established, examining XML/HTTP plugin settings..."
  934. # While we're at it, let the user know whether their settings are
  935. # too restrictive.
  936. #
  937. # XXX: We need a subclass of RemoteMethodError representing
  938. # Access Denied messages.
  939. from dopal.errors import NoSuchMethodError, RemoteMethodError
  940. # Read-only methods?
  941. try:
  942. connection.get_plugin_interface().getTorrentManager()
  943. except RemoteMethodError:
  944. read_only = True
  945. else:
  946. read_only = False
  947. # XXX: Some sort of plugin utility module?
  948. if read_only:
  949. print
  950. print 'NOTE: The XML/HTTP plugin appears to be set to read-only - this may restrict'
  951. print ' scripts from working properly.'
  952. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  953. # Generic classes became the default immediately after 2.4.0.2.
  954. if connection.get_azureus_version() > (2, 4, 0, 2):
  955. generic_classes = True
  956. generic_classes_capable = True
  957. elif connection.get_azureus_version() < (2, 4, 0, 0):
  958. generic_classes = False
  959. generic_classes_capable = False
  960. else:
  961. generic_classes_capable = True
  962. try:
  963. connection.get_plugin_interface().getLogger()
  964. except NoSuchMethodError:
  965. generic_classes = False
  966. else:
  967. generic_classes = True
  968. if not generic_classes:
  969. print
  970. if generic_classes_capable:
  971. print 'NOTE: The XML/HTTP plugin appears to have the "Use generic classes"'
  972. print ' setting disabled. This may prevent some scripts from running'
  973. print ' properly - please consider enabling this setting.'
  974. else:
  975. print 'NOTE: This version of Azureus appears to be older than 2.4.0.0.'
  976. print ' This may prevent some scripts from running properly.'
  977. print ' Please consider upgrading an updated version of Azureus.'
  978. else:
  979. print 'No problems found with XML/HTTP plugin settings.'
  980. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  981. # Is the logging module available?
  982. try:
  983. import logging
  984. except ImportError:
  985. logging_available = False
  986. else:
  987. logging_available = True
  988. # Now we need to figure out what settings have been defined.
  989. #
  990. # In level of importance:
  991. # - Option on command line.
  992. # - Default options for chosen profile.
  993. # - Default global settings.
  994. # Global default settings.
  995. settings = {
  996. 'logging': 'none',
  997. 'needs_connection': 'yes',
  998. 'announce': 'error',
  999. 'pause_on_exit': 'no',
  1000. }
  1001. # Profile default settings.
  1002. #
  1003. # I'll only define those settings which differ from the global defaults.
  1004. settings.update({
  1005. 'background': {
  1006. 'needs_connection': 'no'
  1007. },
  1008. 'command': {
  1009. 'logging': 'none',
  1010. 'announce': 'no',
  1011. },
  1012. 'app': {
  1013. 'logging': 'none',
  1014. 'pause_on_exit': 'error',
  1015. 'announce': 'no',
  1016. },
  1017. None: {},
  1018. }[options.run_mode])
  1019. # Explicitly given settings.
  1020. for setting_name in settings.keys():
  1021. if getattr(options, setting_name) is not None:
  1022. settings[setting_name] = getattr(options, setting_name)
  1023. # Ensure that the user doesn't request logging settings which we can't
  1024. # support.
  1025. #
  1026. # logdest = file or stderr
  1027. # logfile = blah
  1028. # logging -> if local, then log to (default) file.
  1029. if not logging_available and \
  1030. (options.loglevel is not None or \
  1031. settings['logging'] != 'none' or \
  1032. options.logfile or options.logdest):
  1033. _module_msg = "Cannot run - you either need to:\n" + \
  1034. " - Install Python 2.3 or greater\n" + \
  1035. " - the 'logging' module from http://www.red-dove.com/python_logging.html\n" + \
  1036. " - Run the command again without --loglevel or --logging parameters"
  1037. _sys_exit(EXIT_MISSING_MODULE, _module_msg)
  1038. # What log level to use?
  1039. loglevel = None
  1040. if options.loglevel is not None:
  1041. loglevel = getattr(logging, options.loglevel.upper())
  1042. # Now we interpret the arguments given and execute ext_run.
  1043. kwargs = {}
  1044. kwargs['silent_on_connection_error'] = settings['needs_connection'] == 'no'
  1045. kwargs['pause_on_exit'] = {'yes': 1, 'no': 0, 'error': 2}[settings['pause_on_exit']]
  1046. kwargs['remote_notify_on_run'] = settings['announce'] == 'yes'
  1047. kwargs['remote_notify_on_error'] = settings['announce'] in ['yes', 'error']
  1048. # Logging settings.
  1049. if options.logdest == 'stderr':
  1050. setup_logging = True
  1051. logging_to_stderr = True
  1052. else:
  1053. setup_logging = None
  1054. logging_to_stderr = False
  1055. kwargs['setup_logging'] = setup_logging
  1056. kwargs['log_level'] = loglevel
  1057. kwargs['log_to_file'] = options.logdest == 'file' or \
  1058. options.logfile is not None
  1059. kwargs['log_file'] = options.logfile
  1060. # print_error_on_pause:
  1061. # Do we want to print the error? That's a bit tough...
  1062. #
  1063. # If we know that we are logging to stderr, then any internal script
  1064. # error will already be printed, so we won't want to do it in that case.
  1065. #
  1066. # If an error has occurred while setting up, we will let it be printed
  1067. # if we pause on errors, but then we have to suppress it from being
  1068. # reprinted (through sys.excepthook). Otherwise, we can let sys.excepthook
  1069. # handle it.
  1070. #
  1071. # If we aren't logging to stderr, and an internal script error occurs,
  1072. # we can do the same thing as we currently do for setting up errors.
  1073. #
  1074. # However, if we are logging to stderr, we need to remember that setting
  1075. # up errors aren't fed through to the logger, so we should print setting
  1076. # up errors.
  1077. if logging_to_stderr:
  1078. # Print only initialisation errors.
  1079. kwargs['print_error_on_pause'] = 3
  1080. else:
  1081. # Print all errors.
  1082. kwargs['print_error_on_pause'] = 1
  1083. print_traceback_in_ext_run = kwargs['pause_on_exit'] and kwargs['print_error_on_pause']
  1084. abort_if_no_connection()
  1085. from dopal.errors import LinkError, ScriptFunctionError
  1086. # Execute script.
  1087. if function is not None:
  1088. try:
  1089. ext_run(name, function, **kwargs)
  1090. except LinkError, error:
  1091. print "Unable to establish a connection..."
  1092. print " Connection:", error.obj
  1093. print " Error:", error.to_error_string()
  1094. _sys_exit(EXIT_SCRIPT_NOT_EXECUTED)
  1095. except:
  1096. # Override sys.excepthook here.
  1097. #
  1098. # It does two things - firstly, if we know that the traceback
  1099. # has already been printed to stderr, then we suppress it
  1100. # being printed again. Secondly, if the exception is a
  1101. # ScriptFunctionError, it will print the original exception
  1102. # instead.
  1103. import sys
  1104. previous_except_hook = sys.excepthook
  1105. def scripting_except_hook(exc_type, exc_value, exc_tb):
  1106. is_script_function_error = False
  1107. if isinstance(exc_value, ScriptFunctionError):
  1108. exc_value = exc_value.error
  1109. exc_type = exc_value.__class__
  1110. is_script_function_error = True
  1111. if logging_to_stderr and is_script_function_error:
  1112. # Only script function errors will be logged to the
  1113. # logger, so we'll only suppress the printing of this
  1114. # exception if the exception is a scripting function
  1115. # error.
  1116. return
  1117. if print_traceback_in_ext_run:
  1118. return
  1119. previous_except_hook(exc_type, exc_value, exc_tb)
  1120. sys.excepthook = scripting_except_hook
  1121. raise
  1122. return
  1123. if __name__ == '__main__':
  1124. SCRIPT_NAME = 'scripting_main'
  1125. # Verify that the command line arguments are accepted.
  1126. run(SCRIPT_NAME, None)
  1127. # Set up two scripts, one which should work, and the other which will fail.
  1128. # We add in some delays, just so things don't happen too quickly.
  1129. print 'The following code will do 2 things - it will run a script which'
  1130. print 'will work, and then run a script which will fail. This is for'
  1131. print 'testing purposes.'
  1132. print
  1133. def do_something_good(script_env):
  1134. print "DownloadManager:", script_env.connection.get_plugin_interface().getDownloadManager()
  1135. def do_something_bad(script_env):
  1136. print "UploadManager:", script_env.connection.get_plugin_interface().getUploadManager()
  1137. print 'Running good script...'
  1138. run(SCRIPT_NAME, do_something_good)
  1139. print
  1140. print 'Finished running good script, waiting for 4 seconds...'
  1141. import time
  1142. time.sleep(4)
  1143. print
  1144. print 'Running bad script...'
  1145. run(SCRIPT_NAME, do_something_bad)
  1146. print