Ran update_copyright.py.
[hooke.git] / hooke / hooke.py
1 # Copyright (C) 2008-2011 Fabrizio Benedetti
2 #                         Massimo Sandal <devicerandom@gmail.com>
3 #                         Rolf Schmidt <rschmidt@alcor.concordia.ca>
4 #                         W. Trevor King <wking@drexel.edu>
5 #
6 # This file is part of Hooke.
7 #
8 # Hooke is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU Lesser General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
12 #
13 # Hooke is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
16 # Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with Hooke.  If not, see
20 # <http://www.gnu.org/licenses/>.
21
22 """Hooke - A force spectroscopy review & analysis tool.
23 """
24
25 if False: # Queue pickle error debugging code
26     """The Hooke class is passed back from the CommandEngine process
27     to the main process via a :class:`multiprocessing.queues.Queue`,
28     which uses :mod:`pickle` for serialization.  There are a number of
29     objects that are unpicklable, and the error messages are not
30     always helpful.  This block of code hooks you into the Queue's
31     _feed method so you can print out useful tidbits to help find the
32     particular object that is gumming up the pickle works.
33     """
34     import multiprocessing.queues
35     import sys
36     feed = multiprocessing.queues.Queue._feed
37     def new_feed (buffer, notempty, send, writelock, close):
38         def s(obj):
39             print 'SEND:', obj, dir(obj)
40             for a in dir(obj):
41                 attr = getattr(obj, a)
42                 #print '  ', a, attr, type(attr)
43             if obj.__class__.__name__ == 'Hooke':
44                 # Set suspect attributes to None until you resolve the
45                 # PicklingError.  Then fix whatever is breaking the
46                 # pickling.
47                 #obj.commands = None
48                 #obj.drivers = None
49                 #obj.plugins = None
50                 #obj.ui = None
51                 pass
52             sys.stdout.flush()
53             send(obj)
54         feed(buffer, notempty, s, writelock, close)
55     multiprocessing.queues.Queue._feed = staticmethod(new_feed)
56
57 from ConfigParser import NoSectionError
58 import logging
59 import logging.config
60 import multiprocessing
61 import optparse
62 import os.path
63 import Queue
64 import unittest
65 import StringIO
66 import sys
67
68 from . import version
69 from . import engine
70 from . import config as config_mod
71 from . import playlist
72 from . import plugin as plugin_mod
73 from . import driver as driver_mod
74 from . import ui
75
76
77 class Hooke (object):
78     def __init__(self, config=None, debug=0):
79         self.debug = debug
80         default_settings = (config_mod.DEFAULT_SETTINGS
81                             + plugin_mod.default_settings()
82                             + driver_mod.default_settings()
83                             + ui.default_settings())
84         if config == None:
85             config = config_mod.HookeConfigParser(
86                 paths=config_mod.DEFAULT_PATHS,
87                 default_settings=default_settings)
88             config.read()
89         self.config = config
90         self.load_log()
91         self.load_plugins()
92         self.load_drivers()
93         self.load_ui()
94         self.engine = engine.CommandEngine()
95         self.playlists = playlist.Playlists()
96
97     def load_log(self):
98         config_file = StringIO.StringIO()
99         self.config.write(config_file)
100         logging.config.fileConfig(StringIO.StringIO(config_file.getvalue()))
101         # Don't attach the logger because it contains an unpicklable
102         # thread.lock.  Instead, grab it directly every time you need it.
103         #self.log = logging.getLogger('hooke')
104         log = logging.getLogger('hooke')
105         log.debug('config paths: %s' % self.config._config_paths)
106
107     def load_plugins(self):
108         self.plugins = plugin_mod.load_graph(
109             plugin_mod.PLUGIN_GRAPH, self.config, include_section='plugins')
110         self.configure_plugins()
111         self.commands = []
112         for plugin in self.plugins:
113             self.commands.extend(plugin.commands())
114         self.command_by_name = dict(
115             [(c.name, c) for c in self.commands])
116
117     def load_drivers(self):
118         self.drivers = plugin_mod.load_graph(
119             driver_mod.DRIVER_GRAPH, self.config, include_section='drivers')
120         self.configure_drivers()
121
122     def load_ui(self):
123         self.ui = ui.load_ui(self.config)
124         self.configure_ui()
125
126     def configure_plugins(self):
127         for plugin in self.plugins:
128             self._configure_item(plugin)
129
130     def configure_drivers(self):
131         for driver in self.drivers:
132             self._configure_item(driver)
133
134     def configure_ui(self):
135         self._configure_item(self.ui)
136
137     def _configure_item(self, item):
138         conditions = self.config.items('conditions')
139         try:
140             item.config = dict(self.config.items(item.setting_section))
141         except NoSectionError:
142             item.config = {}
143         for key,value in conditions:
144             if key not in item.config:
145                 item.config[key] = value
146
147     def close(self, save_config=False):
148         if save_config == True:
149             self.config.write()  # Does not preserve original comments
150
151     def run_command(self, command, arguments):
152         """Run the command named `command` with `arguments` using
153         :meth:`~hooke.engine.CommandEngine.run_command`.
154
155         Allows for running commands without spawning another process
156         as in :class:`HookeRunner`.
157         """
158         self.engine.run_command(self, command, arguments)
159
160
161 class HookeRunner (object):
162     def run(self, hooke):
163         """Run Hooke's main execution loop.
164
165         Spawns a :class:`hooke.engine.CommandEngine` subprocess and
166         then runs the UI, rejoining the `CommandEngine` process after
167         the UI exits.
168         """
169         ui_to_command,command_to_ui,command = self._setup_run(hooke)
170         try:
171             hooke.ui.run(hooke.commands, ui_to_command, command_to_ui)
172         finally:
173             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
174         return hooke
175
176     def run_lines(self, hooke, lines):
177         """Run the pre-set commands `lines` with the "command line" UI.
178
179         Allows for non-interactive sessions that are otherwise
180         equivalent to :meth:'.run'.
181         """
182         cmdline = ui.load_ui(hooke.config, 'command line')
183         ui_to_command,command_to_ui,command = self._setup_run(hooke)
184         try:
185             cmdline.run_lines(
186                 hooke.commands, ui_to_command, command_to_ui, lines)
187         finally:
188             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
189         return hooke
190
191     def _setup_run(self, hooke):
192         ui_to_command = multiprocessing.Queue()
193         command_to_ui = multiprocessing.Queue()
194         manager = multiprocessing.Manager()
195         command = multiprocessing.Process(name='command engine',
196             target=hooke.engine.run, args=(hooke, ui_to_command, command_to_ui))
197         command.start()
198         hooke.engine = None  # no more need for the UI-side version.
199         return (ui_to_command, command_to_ui, command)
200
201     def _cleanup_run(self, ui_to_command, command_to_ui, command):
202         log = logging.getLogger('hooke')
203         log.debug('cleanup sending CloseEngine')
204         ui_to_command.put(engine.CloseEngine())
205         hooke = None
206         while not isinstance(hooke, Hooke):
207             log.debug('cleanup waiting for Hooke instance from the engine.')
208             hooke = command_to_ui.get(block=True)
209             log.debug('cleanup got %s instance' % type(hooke))
210         command.join()
211         for playlist in hooke.playlists:  # Curve._hooke is not pickled.
212             for curve in playlist:
213                 curve.set_hooke(hooke)
214         return hooke
215
216
217 def main():
218     p = optparse.OptionParser()
219     p.add_option(
220         '--version', dest='version', default=False, action='store_true',
221         help="Print Hooke's version information and exit.")
222     p.add_option(
223         '-s', '--script', dest='script', metavar='FILE',
224         help='Script of command line Hooke commands to run.')
225     p.add_option(
226         '-c', '--command', dest='commands', metavar='COMMAND',
227         action='append', default=[],
228         help='Add a command line Hooke command to run.')
229     p.add_option(
230         '-p', '--persist', dest='persist', action='store_true', default=False,
231         help="Don't exit after running a script or commands.")
232     p.add_option(
233         '-u', '--ui', dest='user_interface',
234         help="Override the configured user interface (for easy switching).")
235     p.add_option(
236         '--config', dest='config', metavar='FILE',
237         help="Override the default config file chain.")
238     p.add_option(
239         '--save-config', dest='save_config',
240         action='store_true', default=False,
241         help="Automatically save a changed configuration on exit.")
242     p.add_option(
243         '--debug', dest='debug', action='store_true', default=False,
244         help="Enable debug logging.")
245     options,arguments = p.parse_args()
246     if len(arguments) > 0:
247         print >> sys.stderr, 'More than 0 arguments to %s: %s' \
248             % (sys.argv[0], arguments)
249         p.print_help(sys.stderr)
250         sys.exit(1)
251     if options.config != None:
252         config_mod.DEFAULT_PATHS = [
253             os.path.abspath(os.path.expanduser(options.config))]
254
255     hooke = Hooke(debug=__debug__)
256     runner = HookeRunner()
257
258     if options.version == True:
259         print version()
260         sys.exit(0)
261     if options.debug == True:
262         hooke.config.set(
263             section='handler_hand1', option='level', value='NOTSET')
264         hooke.load_log()
265     if options.user_interface not in [None, hooke.ui.name]:
266         hooke.config.set(
267             ui.USER_INTERFACE_SETTING_SECTION, hooke.ui.name, False)
268         hooke.config.set(
269             ui.USER_INTERFACE_SETTING_SECTION, options.user_interface, True)
270         hooke.load_ui()
271     if options.script != None:
272         with open(os.path.expanduser(options.script), 'r') as f:
273             options.commands.extend(f.readlines())
274     if len(options.commands) > 0:
275         try:
276             hooke = runner.run_lines(hooke, options.commands)
277         finally:
278             if options.persist == False:
279                 hooke.close(save_config=options.save_config)
280                 sys.exit(0)
281
282     try:
283         hooke = runner.run(hooke)
284     finally:
285         hooke.close(save_config=options.save_config)