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