Add --debug option to bin/hooke and log CommandEngine.run_command()
[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     p.add_option(
200         '--debug', dest='debug', action='store_true', default=False,
201         help="Enable debug logging.")
202     options,arguments = p.parse_args()
203     if len(arguments) > 0:
204         print >> sys.stderr, 'More than 0 arguments to %s: %s' \
205             % (sys.argv[0], arguments)
206         p.print_help(sys.stderr)
207         sys.exit(1)
208
209     hooke = Hooke(debug=__debug__)
210     runner = HookeRunner()
211
212     if options.version == True:
213         print version()
214         sys.exit(0)
215     if options.debug == True:
216         hooke.config.set(
217             section='handler_hand1', option='level', value='NOTSET')
218         hooke.load_log()
219     if options.script != None:
220         with open(os.path.expanduser(options.script), 'r') as f:
221             options.commands.extend(f.readlines())
222     if len(options.commands) > 0:
223         try:
224             hooke = runner.run_lines(hooke, options.commands)
225         finally:
226             if options.command_exit == True:
227                 hooke.close()
228                 sys.exit(0)
229
230     try:
231         hooke = runner.run(hooke)
232     finally:
233         hooke.close()