1# -*- coding: utf-8 -*- 2# 3# __init__.py - commander 4# 5# Copyright (C) 2010 - Jesse van den Kieboom 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, 20# Boston, MA 02110-1301, USA. 21 22from gi.repository import GLib, GObject, Gio 23 24import sys, bisect, types, shlex, re, os, traceback 25 26from . import module, method, result, exceptions, metamodule, completion 27 28from commands.accel_group import AccelGroup 29from commands.accel_group import Accelerator 30 31__all__ = ['is_commander_module', 'Commands', 'Accelerator'] 32 33import commander.modules 34 35def attrs(**kwargs): 36 def generator(f): 37 for k in kwargs: 38 setattr(f, k, kwargs[k]) 39 40 return f 41 42 return generator 43 44def autocomplete(d={}, **kwargs): 45 ret = {} 46 47 for dic in (d, kwargs): 48 for k in dic: 49 if type(dic[k]) == types.FunctionType: 50 ret[k] = dic[k] 51 52 return attrs(autocomplete=ret) 53 54def accelerator(*args, **kwargs): 55 return attrs(accelerator=Accelerator(args, kwargs)) 56 57def is_commander_module(mod): 58 if type(mod) == types.ModuleType: 59 return mod and ('__commander_module__' in mod.__dict__) 60 else: 61 mod = str(mod) 62 return mod.endswith('.py') or (os.path.isdir(mod) and os.path.isfile(os.path.join(mod, '__init__.py'))) 63 64class Singleton(object): 65 _instance = None 66 67 def __new__(cls, *args, **kwargs): 68 if not cls._instance: 69 cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs) 70 cls._instance.__init_once__() 71 72 return cls._instance 73 74class Commands(Singleton): 75 class Continuated: 76 def __init__(self, generator): 77 self.generator = generator 78 self.retval = None 79 80 def autocomplete_func(self): 81 mod = sys.modules['commander.commands.result'] 82 83 if self.retval == mod.Result.PROMPT: 84 return self.retval.autocomplete 85 else: 86 return {} 87 88 def args(self): 89 return [], True 90 91 class State: 92 def __init__(self): 93 self.clear() 94 95 def clear(self): 96 self.stack = [] 97 98 def top(self): 99 return self.stack[0] 100 101 def run(self, ret): 102 ct = self.top() 103 104 if ret: 105 ct.retval = ct.generator.send(ret) 106 else: 107 ct.retval = next(ct.generator) 108 109 return ct.retval 110 111 def push(self, gen): 112 self.stack.insert(0, Commands.Continuated(gen)) 113 114 def pop(self): 115 if not self.stack: 116 return 117 118 try: 119 self.stack[0].generator.close() 120 except GeneratorExit: 121 pass 122 123 del self.stack[0] 124 125 def __len__(self): 126 return len(self.stack) 127 128 def __nonzero__(self): 129 return len(self) != 0 130 131 def __init_once__(self): 132 self._modules = None 133 self._dirs = [] 134 self._monitors = [] 135 self._accel_group = None 136 137 self._timeouts = {} 138 139 self._stack = [] 140 141 def set_dirs(self, dirs): 142 self._dirs = dirs 143 144 def stop(self): 145 for mon in self._monitors: 146 mon.cancel() 147 148 self._monitors = [] 149 self._modules = None 150 151 for k in self._timeouts: 152 GLib.source_remove(self._timeouts[k]) 153 154 self._timeouts = {} 155 156 def accelerator_activated(self, accel, mod, state, entry): 157 self.run(state, mod.execute('', [], entry, 0, accel.arguments)) 158 159 def scan_accelerators(self, modules=None): 160 if modules == None: 161 self._accel_group = AccelGroup() 162 modules = self.modules() 163 164 recurse_mods = [] 165 166 for mod in modules: 167 if type(mod) == types.ModuleType: 168 recurse_mods.append(mod) 169 else: 170 accel = mod.accelerator() 171 172 if accel != None: 173 self._accel_group.add(accel, self.accelerator_activated, mod) 174 175 for mod in recurse_mods: 176 self.scan_accelerators(mod.commands()) 177 178 def accelerator_group(self): 179 if not self._accel_group: 180 self.scan_accelerators() 181 182 return self._accel_group 183 184 def modules(self): 185 self.ensure() 186 return list(self._modules) 187 188 def add_monitor(self, d): 189 gfile = Gio.file_new_for_path(d) 190 monitor = None 191 192 try: 193 monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None) 194 except Exception as e: 195 # Could not create monitor, this happens on systems where file monitoring is 196 # not supported, but we don't really care 197 pass 198 199 if monitor: 200 monitor.connect('changed', self.on_monitor_changed) 201 self._monitors.append(monitor) 202 203 def scan(self, d): 204 files = [] 205 206 try: 207 files = os.listdir(d) 208 except OSError: 209 pass 210 211 for f in files: 212 full = os.path.join(d, f) 213 214 # Test for python files or modules 215 if is_commander_module(full): 216 if self.add_module(full) and os.path.isdir(full): 217 # Add monitor on the module directory if module was 218 # successfully added. TODO: recursively add monitors 219 self.add_monitor(full) 220 221 # Add a monitor on the scanned directory itself 222 self.add_monitor(d) 223 224 def module_name(self, filename): 225 # Module name is the basename without the .py 226 return os.path.basename(os.path.splitext(filename)[0]) 227 228 def add_module(self, filename): 229 base = self.module_name(filename) 230 231 # Check if module already exists 232 if base in self._modules: 233 return 234 235 # Create new 'empty' module 236 mod = module.Module(base, os.path.dirname(filename)) 237 bisect.insort_right(self._modules, mod) 238 239 # Reload the module 240 self.reload_module(mod) 241 return True 242 243 def ensure(self): 244 # Ensure that modules have been scanned 245 if self._modules != None: 246 return 247 248 self._modules = [] 249 250 for d in self._dirs: 251 self.scan(d) 252 253 def _run_generator(self, state, ret=None): 254 mod = sys.modules['commander.commands.result'] 255 256 try: 257 # Determine first use 258 retval = state.run(ret) 259 260 if not retval or (isinstance(retval, mod.Result) and (retval == mod.DONE or retval == mod.HIDE)): 261 state.pop() 262 263 if state: 264 return self._run_generator(state) 265 266 return self.run(state, retval) 267 268 except StopIteration: 269 state.pop() 270 271 if state: 272 return self.run(state) 273 except Exception as e: 274 # Something error like, we throw on the parent generator 275 state.pop() 276 277 if state: 278 state.top().generator.throw(type(e), e, e.__traceback__) 279 else: 280 # Re raise it for the top most to show the error 281 raise 282 283 return None 284 285 def run(self, state, ret=None): 286 mod = sys.modules['commander.commands.result'] 287 288 if type(ret) == types.GeneratorType: 289 # Ok, this is cool stuff, generators can ask and susped execution 290 # of commands, for instance to prompt for some more information 291 state.push(ret) 292 293 return self._run_generator(state) 294 elif not isinstance(ret, mod.Result) and len(state) > 1: 295 # Basicly, send it to the previous? 296 state.pop() 297 298 return self._run_generator(state, ret) 299 else: 300 return ret 301 302 def execute(self, state, argstr, words, wordsstr, entry, modifier): 303 self.ensure() 304 305 if state: 306 return self._run_generator(state, [argstr, words, modifier]) 307 308 cmd = completion.single_command(wordsstr, 0) 309 310 if not cmd: 311 raise exceptions.Execute('Could not find command: ' + wordsstr[0]) 312 313 if len(words) > 1: 314 argstr = argstr[words[1].start(0):] 315 else: 316 argstr = '' 317 318 # Execute command 319 return self.run(state, cmd.execute(argstr, wordsstr[1:], entry, modifier)) 320 321 def invoke(self, entry, modifier, command, args, argstr=None): 322 self.ensure() 323 324 cmd = completion.single_command([command], 0) 325 326 if not cmd: 327 raise exceptions.Execute('Could not find command: ' + command) 328 329 if argstr == None: 330 argstr = ' '.join(args) 331 332 ret = cmd.execute(argstr, args, entry, modifier) 333 334 if type(ret) == types.GeneratorType: 335 raise exceptions.Execute('Cannot invoke commands that yield (yet)') 336 else: 337 return ret 338 339 def resolve_module(self, path, load=True): 340 if not self._modules or not is_commander_module(path): 341 return None 342 343 # Strip off __init__.py for module kind of modules 344 if path.endswith('__init__.py'): 345 path = os.path.dirname(path) 346 347 base = self.module_name(path) 348 349 # Find module 350 idx = bisect.bisect_left(self._modules, base) 351 mod = None 352 353 if idx < len(self._modules): 354 mod = self._modules[idx] 355 356 if not mod or mod.name != base: 357 if load: 358 self.add_module(path) 359 360 return None 361 362 return mod 363 364 def remove_module_accelerators(self, modules): 365 recurse_mods = [] 366 367 for mod in modules: 368 if type(mod) == types.ModuleType: 369 recurse_mods.append(mod) 370 else: 371 accel = mod.accelerator() 372 373 if accel != None: 374 self._accel_group.remove(accel) 375 376 for mod in recurse_mods: 377 self.remove_module_accelerators(mod.commands()) 378 379 def remove_module(self, mod): 380 # Remove roots 381 for r in mod.roots(): 382 if r in self._modules: 383 self._modules.remove(r) 384 385 # Remove accelerators 386 if self._accel_group: 387 self.remove_module_accelerators([mod]) 388 389 if mod.name in commander.modules.__dict__: 390 del commander.modules.__dict__[mod.name] 391 392 def reload_module(self, mod): 393 if isinstance(mod, str): 394 mod = self.resolve_module(mod) 395 396 if not mod or not self._modules: 397 return 398 399 # Remove roots 400 self.remove_module(mod) 401 402 # Now, try to reload the module 403 try: 404 mod.reload() 405 except Exception as e: 406 # Reload failed, we remove the module 407 info = traceback.format_tb(sys.exc_info()[2]) 408 print('Failed to reload module ({0}):\n {1}'.format(mod.name, info[-1])) 409 410 self._modules.remove(mod) 411 return 412 413 # Insert roots 414 for r in mod.roots(): 415 bisect.insort(self._modules, r) 416 417 commander.modules.__dict__[mod.name] = metamodule.MetaModule(mod.mod) 418 419 if self._accel_group: 420 self.scan_accelerators([mod]) 421 422 def on_timeout_delete(self, path, mod): 423 if not path in self._timeouts: 424 return False 425 426 # Remove the module 427 mod.unload() 428 self.remove_module(mod) 429 self._modules.remove(mod) 430 431 return False 432 433 def on_monitor_changed(self, monitor, gfile1, gfile2, evnt): 434 if evnt == Gio.FileMonitorEvent.CHANGED: 435 # Reload the module 436 self.reload_module(gfile1.get_path()) 437 elif evnt == Gio.FileMonitorEvent.DELETED: 438 path = gfile1.get_path() 439 mod = self.resolve_module(path, False) 440 441 if not mod: 442 return 443 444 if path in self._timeouts: 445 GLib.source_remove(self._timeouts[path]) 446 447 # We add a timeout because a common save strategy causes a 448 # DELETE/CREATE event chain 449 self._timeouts[path] = GLib.timeout_add(500, self.on_timeout_delete, path, mod) 450 elif evnt == Gio.FileMonitorEvent.CREATED: 451 path = gfile1.get_path() 452 453 # Check if this CREATE followed a previous DELETE 454 if path in self._timeouts: 455 GLib.source_remove(self._timeouts[path]) 456 del self._timeouts[path] 457 458 # Reload the module 459 self.reload_module(path) 460 461# ex:ts=4:et 462