3b81c24eed93499f0f3940c48139b2a6a2e4febb
[hooke.git] / hooke / plugin / command_stack.py
1 # Copyright (C) 2008-2010 Alberto Gomez-Casado
2 #                         Massimo Sandal <devicerandom@gmail.com>
3 #                         W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
7 # Hooke is free software: you can redistribute it and/or modify it
8 # under the terms of the GNU Lesser General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
11 #
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
15 # Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke.  If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 """The ``command_stack`` module provides :class:`CommandStackPlugin`
22 and several associated :class:`~hooke.command.Command`\s exposing
23 :mod`hooke.command_stack`'s functionality.
24 """
25
26 import logging
27 import os.path
28 from Queue import Queue
29
30 from ..command import Command, Argument, Success, Failure
31 from ..command_stack import FileCommandStack
32 from ..config import Setting
33 from ..engine import CloseEngine, CommandMessage
34 from . import Builtin
35
36
37 # Define useful command subclasses
38
39 class CommandStackCommand (Command):
40     """Subclass to avoid pushing control commands to the stack.
41     """
42     def _set_state(self, state):
43         try:
44             self.plugin.set_state(state)
45         except ValueError, e:
46             self.plugin.log('raising error: %s' % e)
47             raise Failure('invalid state change: %s' % e.state_change)
48
49
50 class CaptureCommand (CommandStackCommand):
51     """Run a mock-engine and save the incoming commands.
52
53     Notes
54     -----
55     Due to limitations in the --script and --command option
56     implementations in ./bin/hooke, capture sessions will die at the
57     end of the script and command execution before entering
58     --persist's interactive session.
59     """
60     def __init__(self, name, plugin):
61         super(CaptureCommand, self).__init__(
62             name=name, help=self.__doc__, plugin=plugin)
63
64     def _run(self, hooke, inqueue, outqueue, params):
65         # TODO: possibly merge code with CommandEngine.run()
66
67         # Fake successful completion so UI continues sending commands.
68         outqueue.put(Success())
69
70         while True:
71             msg = inqueue.get()
72             if isinstance(msg, CloseEngine):
73                 outqueue.put('CloseEngine')
74                 inqueue.put(msg)  # Put CloseEngine back for CommandEngine.
75                 self._set_state('inactive')
76                 return
77             assert isinstance(msg, CommandMessage), type(msg)
78             cmd = hooke.command_by_name[msg.command]
79             if isinstance(cmd, CommandStackCommand):
80                 if isinstance(cmd, StopCaptureCommand):
81                     outqueue = Queue()  # Grab StopCaptureCommand's completion.
82                 cmd.run(hooke, inqueue, outqueue, **msg.arguments)
83                 if isinstance(cmd, StopCaptureCommand):
84                     assert self.plugin.state == 'inactive', self.plugin.state
85                     # Return the stolen completion as our own.
86                     raise outqueue.get(block=False)
87             else:
88                 self.plugin.log('appending %s' % msg)
89                 self.plugin.command_stack.append(msg)
90                 # Fake successful completion so UI continues sending commands.
91                 outqueue.put(Success())
92
93
94 # The plugin itself
95
96 class CommandStackPlugin (Builtin):
97     """Commands for managing a command stack (similar to macros).
98     """
99     def __init__(self):
100         super(CommandStackPlugin, self).__init__(name='command_stack')
101         self._commands = [
102             StartCaptureCommand(self), StopCaptureCommand(self),
103             ReStartCaptureCommand(self),
104             PopCommand(self), GetCommand(self), GetStateCommand(self),
105             SaveCommand(self), LoadCommand(self),
106             ]
107         self._settings = [
108             Setting(section=self.setting_section, help=self.__doc__),
109             Setting(section=self.setting_section, option='path',
110                     value=os.path.join('resources', 'command_stack'),
111                     help='Directory containing command stack files.'), # TODO: allow colon separated list, like $PATH.
112             ]
113         self.command_stack = FileCommandStack()
114         self.state = 'inactive'
115         # inactive <-> active.
116         self._valid_transitions = {
117             'inactive': ['active'],
118             'active': ['inactive'],
119             }
120
121     def default_settings(self):
122         return self._settings
123
124     def log(self, msg):
125         log = logging.getLogger('hooke')
126         log.debug('%s %s' % (self.name, msg))
127
128     def set_state(self, state):
129         state_change = '%s -> %s' % (self.state, state)
130         self.log('changing state: %s' % state_change)
131         if state not in self._valid_transitions[self.state]:
132             e = ValueError(state)
133             e.state_change = state_change
134             raise e
135         self.state = state
136
137
138 # Define commands
139
140 class StartCaptureCommand (CaptureCommand):
141     """Clear any previous stack and run the mock-engine.
142     """
143     def __init__(self, plugin):
144         super(StartCaptureCommand, self).__init__(
145             name='start command capture', plugin=plugin)
146
147     def _run(self, hooke, inqueue, outqueue, params):
148         self._set_state('active')
149         self.plugin.command_stack = FileCommandStack()  # clear command stack
150         super(StartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
151
152
153 class ReStartCaptureCommand (CaptureCommand):
154     """Run the mock-engine.
155     """
156     def __init__(self, plugin):
157         super(ReStartCaptureCommand, self).__init__(
158             name='restart command capture', plugin=plugin)
159
160     def _run(self, hooke, inqueue, outqueue, params):
161         self._set_state('active')
162         super(ReStartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
163
164
165 class StopCaptureCommand (CommandStackCommand):
166     """Stop the mock-engine.
167     """
168     def __init__(self, plugin):
169         super(StopCaptureCommand, self).__init__(
170             name='stop command capture', help=self.__doc__, plugin=plugin)
171
172     def _run(self, hooke, inqueue, outqueue, params):
173         self._set_state('inactive')
174
175
176 class PopCommand (CommandStackCommand):
177     """Pop the top command off the stack.
178     """
179     def __init__(self, plugin):
180         super(PopCommand, self).__init__(
181             name='pop command from stack', help=self.__doc__, plugin=plugin)
182
183     def _run(self, hooke, inqueue, outqueue, params):
184         outqueue.put(self.plugin.command_stack.pop())
185
186
187 class GetCommand (CommandStackCommand):
188     """Return the command stack.
189     """
190     def __init__(self, plugin):
191         super(GetCommand, self).__init__(
192             name='get command stack', help=self.__doc__, plugin=plugin)
193
194     def _run(self, hooke, inqueue, outqueue, params):
195         outqueue.put(self.plugin.command_stack)
196
197 class GetStateCommand (CommandStackCommand):
198     """Return the mock-engine state.
199     """
200     def __init__(self, plugin):
201         super(GetStateCommand, self).__init__(
202             name='get command capture state', help=self.__doc__, plugin=plugin)
203
204     def _run(self, hooke, inqueue, outqueue, params):
205         outqueue.put(self.plugin.state)
206
207
208 class SaveCommand (CommandStackCommand):
209     """Save the command stack.
210     """
211     def __init__(self, plugin):
212         super(SaveCommand, self).__init__(
213             name='save command stack',
214             arguments=[
215                 Argument(name='output', type='file',
216                          help="""
217 File name for the output command stack.  Defaults to overwriting the
218 input command stack.  If the command stack does not have an input file
219 (e.g. it is the default) then the file name defaults to `default`.
220 """.strip()),
221                 ],
222             help=self.__doc__, plugin=plugin)
223
224     def _run(self, hooke, inqueue, outqueue, params):
225         params = self.__setup_params(hooke, params)
226         self.plugin.command_stack.save(params['output'])
227
228     def __setup_params(self, hooke, params):
229         if params['output'] == None and self.plugin.command_stack.path == None:
230             params['output'] = 'default'
231         if params['output'] != None:
232             params['output'] = os.path.join(
233                 self.plugin.config['path'], params['output'])
234         return params
235
236 class LoadCommand (CommandStackCommand):
237     """Load the command stack.
238
239     .. warning:: This is *not safe* with untrusted input.
240     """
241     def __init__(self, plugin):
242         super(LoadCommand, self).__init__(
243             name='load command stack',
244             arguments=[
245                 Argument(name='input', type='file',
246                          help="""
247 File name for the input command stack.
248 """.strip()),
249                 ],
250             help=self.__doc__, plugin=plugin)
251
252     def _run(self, hooke, inqueue, outqueue, params):
253         params = self.__setup_params(hooke, params)
254         self.plugin.command_stack.clear()
255         self.plugin.command_stack.load(params['input'])
256
257     def __setup_params(self, hooke, params):
258         if params['input'] == None and self.plugin.command_stack.path == None:
259             params['input'] = 'default'
260         if params['input'] != None:
261             params['input'] = os.path.join(
262                 self.plugin.config['path'], params['input'])
263         return params