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