cf6600ed8714fc8e6b4916a317935d79bb3e5a61
[hooke.git] / hooke / hooke.py
1 # Copyright (C) 2008-2010 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 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.NoteIndexList()
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
104     def load_plugins(self):
105         self.plugins = plugin_mod.load_graph(
106             plugin_mod.PLUGIN_GRAPH, self.config, include_section='plugins')
107         self.commands = []
108         for plugin in self.plugins:
109             self.commands.extend(plugin.commands())
110         self.command_by_name = dict(
111             [(c.name, c) for c in self.commands])
112
113     def load_drivers(self):
114         self.drivers = plugin_mod.load_graph(
115             driver_mod.DRIVER_GRAPH, self.config, include_section='drivers')
116
117     def load_ui(self):
118         self.ui = ui.load_ui(self.config)
119
120     def close(self):
121         self.config.write() # Does not preserve original comments
122
123     def run_command(self, command, arguments):
124         """Run `command` with `arguments` using
125         :meth:`~hooke.engine.CommandEngine.run_command`.
126
127         Allows for running commands without spawning another process
128         as in :class:`HookeRunner`.
129         """
130         self.engine.run_command(self, command, arguments)
131
132
133 class HookeRunner (object):
134     def run(self, hooke):
135         """Run Hooke's main execution loop.
136
137         Spawns a :class:`hooke.engine.CommandEngine` subprocess and
138         then runs the UI, rejoining the `CommandEngine` process after
139         the UI exits.
140         """
141         ui_to_command,command_to_ui,command = self._setup_run(hooke)
142         try:
143             hooke.ui.run(hooke.commands, ui_to_command, command_to_ui)
144         finally:
145             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
146         return hooke
147
148     def run_lines(self, hooke, lines):
149         """Run the pre-set commands `lines` with the "command line" UI.
150
151         Allows for non-interactive sessions that are otherwise
152         equivalent to :meth:'.run'.
153         """
154         cmdline = ui.load_ui(hooke.config, 'command line')
155         ui_to_command,command_to_ui,command = self._setup_run(hooke)
156         try:
157             cmdline.run_lines(
158                 hooke.commands, ui_to_command, command_to_ui, lines)
159         finally:
160             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
161         return hooke
162
163     def _setup_run(self, hooke):
164         ui_to_command = multiprocessing.Queue()
165         command_to_ui = multiprocessing.Queue()
166         manager = multiprocessing.Manager()
167         command = multiprocessing.Process(name='command engine',
168             target=hooke.engine.run, args=(hooke, ui_to_command, command_to_ui))
169         command.start()
170         return (ui_to_command, command_to_ui, command)
171
172     def _cleanup_run(self, ui_to_command, command_to_ui, command):
173         log = logging.getLogger('hooke')
174         log.debug('cleanup sending CloseEngine')
175         ui_to_command.put(engine.CloseEngine())
176         hooke = None
177         while not isinstance(hooke, Hooke):
178             log.debug('cleanup waiting for Hooke instance from the engine.')
179             hooke = command_to_ui.get(block=True)
180             log.debug('cleanup got %s instance' % type(hooke))
181         command.join()
182         return hooke
183
184
185 def main():
186     p = optparse.OptionParser()
187     p.add_option(
188         '--version', dest='version', default=False, action='store_true',
189         help="Print Hooke's version information and exit.")
190     p.add_option(
191         '-s', '--script', dest='script', metavar='FILE',
192         help='Script of command line Hooke commands to run.')
193     p.add_option(
194         '-c', '--command', dest='commands', metavar='COMMAND',
195         action='append', default=[],
196         help='Add a command line Hooke command to run.')
197     p.add_option(
198         '--command-no-exit', dest='command_exit',
199         action='store_false', default=True,
200         help="Don't exit after running a script or commands.")
201     p.add_option(
202         '--debug', dest='debug', action='store_true', default=False,
203         help="Enable debug logging.")
204     options,arguments = p.parse_args()
205     if len(arguments) > 0:
206         print >> sys.stderr, 'More than 0 arguments to %s: %s' \
207             % (sys.argv[0], arguments)
208         p.print_help(sys.stderr)
209         sys.exit(1)
210
211     hooke = Hooke(debug=__debug__)
212     runner = HookeRunner()
213
214     if options.version == True:
215         print version()
216         sys.exit(0)
217     if options.debug == True:
218         hooke.config.set(
219             section='handler_hand1', option='level', value='NOTSET')
220         hooke.load_log()
221     if options.script != None:
222         with open(os.path.expanduser(options.script), 'r') as f:
223             options.commands.extend(f.readlines())
224     if len(options.commands) > 0:
225         try:
226             hooke = runner.run_lines(hooke, options.commands)
227         finally:
228             if options.command_exit == True:
229                 hooke.close()
230                 sys.exit(0)
231
232     try:
233         hooke = runner.run(hooke)
234     finally:
235         hooke.close()