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