Added command stack saving and loading.
[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 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.NoteIndexList()
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
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         return hooke
210
211
212 def main():
213     p = optparse.OptionParser()
214     p.add_option(
215         '--version', dest='version', default=False, action='store_true',
216         help="Print Hooke's version information and exit.")
217     p.add_option(
218         '-s', '--script', dest='script', metavar='FILE',
219         help='Script of command line Hooke commands to run.')
220     p.add_option(
221         '-c', '--command', dest='commands', metavar='COMMAND',
222         action='append', default=[],
223         help='Add a command line Hooke command to run.')
224     p.add_option(
225         '-p', '--persist', dest='persist', action='store_true', default=False,
226         help="Don't exit after running a script or commands.")
227     p.add_option(
228         '--save-config', dest='save_config',
229         action='store_true', default=False,
230         help="Automatically save a changed configuration on exit.")
231     p.add_option(
232         '--debug', dest='debug', action='store_true', default=False,
233         help="Enable debug logging.")
234     options,arguments = p.parse_args()
235     if len(arguments) > 0:
236         print >> sys.stderr, 'More than 0 arguments to %s: %s' \
237             % (sys.argv[0], arguments)
238         p.print_help(sys.stderr)
239         sys.exit(1)
240
241     hooke = Hooke(debug=__debug__)
242     runner = HookeRunner()
243
244     if options.version == True:
245         print version()
246         sys.exit(0)
247     if options.debug == True:
248         hooke.config.set(
249             section='handler_hand1', option='level', value='NOTSET')
250         hooke.load_log()
251     if options.script != None:
252         with open(os.path.expanduser(options.script), 'r') as f:
253             options.commands.extend(f.readlines())
254     if len(options.commands) > 0:
255         try:
256             hooke = runner.run_lines(hooke, options.commands)
257         finally:
258             if options.persist == False:
259                 hooke.close(save_config=options.save_config)
260                 sys.exit(0)
261
262     try:
263         hooke = runner.run(hooke)
264     finally:
265         hooke.close(save_config=options.save_config)