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