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