Merged Rolf Schmidt's illysam branch
[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
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation, either
11 # version 3 of the License, or (at your option) any later version.
12 #
13 # Hooke is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU Lesser General 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 unittest
63 import StringIO
64 import sys
65
66 from . import engine as engine
67 from . import config as config_mod
68 from . import playlist as playlist
69 from . import plugin as plugin_mod
70 from . import driver as driver_mod
71 from . import ui as ui
72
73
74 class Hooke (object):
75     def __init__(self, config=None, debug=0):
76         self.debug = debug
77         default_settings = (config_mod.DEFAULT_SETTINGS
78                             + plugin_mod.default_settings()
79                             + driver_mod.default_settings()
80                             + ui.default_settings())
81         if config == None:
82             config = config_mod.HookeConfigParser(
83                 paths=config_mod.DEFAULT_PATHS,
84                 default_settings=default_settings)
85             config.read()
86         self.config = config
87         self.load_log()
88         self.load_plugins()
89         self.load_drivers()
90         self.load_ui()
91         self.command = engine.CommandEngine()
92         self.playlists = playlist.NoteIndexList()
93
94     def load_log(self):
95         config_file = StringIO.StringIO()
96         self.config.write(config_file)
97         x = config_file.getvalue()
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
103     def load_plugins(self):
104         self.plugins = plugin_mod.load_graph(
105             plugin_mod.PLUGIN_GRAPH, self.config, include_section='plugins')
106         self.commands = []
107         for plugin in self.plugins:
108             self.commands.extend(plugin.commands())
109
110     def load_drivers(self):
111         self.drivers = plugin_mod.load_graph(
112             driver_mod.DRIVER_GRAPH, self.config, include_section='drivers')
113
114     def load_ui(self):
115         self.ui = ui.load_ui(self.config)
116
117     def close(self):
118         self.config.write() # Does not preserve original comments
119
120 class HookeRunner (object):
121     def run(self, hooke):
122         """Run Hooke's main execution loop.
123
124         Spawns a :class:`hooke.engine.CommandEngine` subprocess and
125         then runs the UI, rejoining the `CommandEngine` process after
126         the UI exits.
127         """
128         ui_to_command,command_to_ui,command = self._setup_run(hooke)
129         try:
130             hooke.ui.run(hooke.commands, ui_to_command, command_to_ui)
131         finally:
132             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
133         return hooke
134
135     def run_lines(self, hooke, lines):
136         """Run the pre-set commands `lines` with the "command line" UI.
137
138         Allows for non-interactive sessions that are otherwise
139         equivalent to :meth:'.run'.
140         """
141         cmdline = ui.load_ui(hooke.config, 'command line')
142         ui_to_command,command_to_ui,command = self._setup_run(hooke)
143         try:
144             cmdline.run_lines(
145                 hooke.commands, ui_to_command, command_to_ui, lines)
146         finally:
147             hooke = self._cleanup_run(ui_to_command, command_to_ui, command)
148         return hooke
149
150     def _setup_run(self, hooke):
151         ui_to_command = multiprocessing.Queue()
152         command_to_ui = multiprocessing.Queue()
153         manager = multiprocessing.Manager()
154         command = multiprocessing.Process(name='command engine',
155             target=hooke.command.run, args=(hooke, ui_to_command, command_to_ui))
156         command.start()
157         return (ui_to_command, command_to_ui, command)
158
159     def _cleanup_run(self, ui_to_command, command_to_ui, command):
160         ui_to_command.put(ui.CloseEngine())
161         hooke = command_to_ui.get()
162         assert isinstance(hooke, Hooke)
163         command.join()
164         return hooke
165
166
167 def main():
168     p = optparse.OptionParser()
169     p.add_option(
170         '-s', '--script', dest='script', metavar='FILE',
171         help='Script of command line Hooke commands to run.')
172     p.add_option(
173         '-c', '--command', dest='commands', metavar='COMMAND',
174         action='append', default=[],
175         help='Add a command line Hooke command to run.')
176     options,arguments = p.parse_args()
177     if len(arguments) > 0:
178         print >> sys.stderr, 'More than 0 arguments to %s: %s' \
179             % (sys.argv[0], arguments)
180         p.print_help(sys.stderr)
181         sys.exit(1)
182
183     hooke = Hooke(debug=__debug__)
184     runner = HookeRunner()
185
186     if options.script != None:
187         f = open(os.path.expanduser(options.script), 'r')
188         options.commands.extend(f.readlines())
189         f.close
190     if len(options.commands) > 0:
191         try:
192             hooke = runner.run_lines(hooke, options.commands)
193         finally:
194             hooke.close()
195         sys.exit(0)
196
197     try:
198         hooke = runner.run(hooke)
199     finally:
200         hooke.close()