#!/bin/bash /usr/scripts/python.sh # -*- coding: utf-8 -*- # # Copyright (C) Valentin Samir # Licence : GPLv3 u""" Module implémentant la "Continuation Passing Style". Le concept est d'exécuter des fonctions qui sont des TailCallers, qui elles-mêmes seront amenées à appeler d'autres fonctions, qui sont des TailCalls, ou qui lèvent des exceptions de type Continue(TailCall). Ces TailCalls pourraient être amenés à exécuter d'autres fonctions, dans ce cas, pour faire en sorte que leur empreinte mémoire soit faible, auquel cas, soit ces fonctions sont elles-mêmes des TailCalls, soit ce sont des TailCallers. L'intérêt de ce concept est de favoriser une stack à empreinte basse. """ import os import sys import time import ldap import signal import inspect import traceback if '/usr/scripts' not in sys.path: sys.path.append('/usr/scripts') from pythondialog import Dialog as PythonDialog from pythondialog import DialogTerminatedBySignal, PythonDialogErrorBeforeExecInChildProcess from pythondialog import error as DialogError from gestion import affichage from cranslib import clogger # Modifier level à debug pour débugguer. LOGGER = clogger.CLogger('gest_crans_lc', service="CPS", level='info') ENCODING = affichage.guess_preferred_encoding() def mydebug(txt): """Petite fonction pour logguer du debug avec CLogger. LOGGER.debug n'écrit que si level est à debug""" if isinstance(txt, list): txt = "\n".join(txt) if isinstance(txt, unicode): txt = txt.encode(ENCODING) LOGGER.debug(txt) class Continue(Exception): """Exception pour envoyer des TailCall en les raisant l'argument tailCall est soit une fonction décorée, soit la fonction retournante elle-même.""" def __init__(self, tailCall): self.tailCall = tailCall # Implémentation "à la main" de la tail récursion en python # voir http://kylem.net/programming/tailcall.html # je trouve ça assez sioux # En plus, ça nous permet de gérer plus facilement le # code en CPS (Continuation Passing Style) class TailCaller(object) : """ Un TailCaller est le doux nom d'un objet (une fonction décorée) qui crée et joue avec des TailCall. Les TailCalls sont des jouets qui servent à réduire l'empreinte dans la stack d'appels récursifs. Lorsqu'on __call__ un TailCaller, celui-ci exécute la fonction qu'il décore. Si le retour de celle-ci est un TailCall, on exécute le call de celui-ci, et ainsi de suite, jusqu'à avoir un retour qui ne soit pas un TailCall. Un call ou la fonction du TailCaller peuvent retourner des erreurs de type continue, qui généralement contiennent un TailCall, mais peuvent aussi contenir une sortie. """ other_callers = {} def __init__(self, f): """On prend f et son nom et on les stocke dans le TailCaller, wouhou, c'est trop fou.""" self.f = f self.func_name = f.func_name TailCaller.other_callers[id(self)]=f.func_name def __del__(self): """Supprime le TailCaller de la liste""" del(TailCaller.other_callers[id(self)]) def __call__(self, *args, **kwargs): """Quand on appelle le TailCaller pour de vrai, on vérifie la position actuelle du bordel dans la stack, et on exécute la fonction référencée, et les TailCalls qu'on peut croiser sur la route, pour peu que leur position en stack soit postérieure à celle du TailCaller actuel.""" mydebug("***%s calling" % self.func_name) stacklvl = len(inspect.stack()) # On applique f une première fois aux arguments # qui vont bien. # Cela peut retourner un TailCall, un résultat, ou # lever une erreur de type Continue. Souvent les fonctions # raisent des Continue contenant des TailCall, dans le but # de ne pas augmenter le contenu de la stack. try: ret = self.f(*args, **kwargs) except Continue as c: # Le TailCall est là-dedans. ret = c.tailCall # Si f a terminé, ret n'est pas un TailCall mais sa valeur # de retour. Sinon, on entre dans la boucle. while isinstance(ret, TailCall): # Ici, on regarde l'endroit dans la stack de l'objet courant # vis-à-vis de la position dans la stack de ret. (plus le stack # lvl est élevé, plus on a été appelé récemment) # On constate donc ici qu'on ne s'intéresse qu'aux TailCall qui # ont été créés après l'appel du TailCaller actuel. if stacklvl >= ret.stacklvl: mydebug("***%s terminate" % self.func_name) # Ici, on peut donc raise un Continue, qui fait sortir # de l'exécution du TailCaller courant. raise Continue(ret) mydebug("***%s doing %s" % (self.func_name, ret)) # L'appel à un TailCall se fait via la méthode handle. # Si ça retourne une continuation, on récupère le tailCall # dedans dans ret, et on boucle. Sinon, soit le TailCall # retourne un TailCall, soit il retourne un résultat. try: ret = ret.handle() except Continue as c: ret = c.tailCall mydebug("***%s returning %s" % (self.func_name, ret)) return ret def tailcaller(f): """Décorateur permettant aux fonctions d'une classe d'être décorées comme TailCaller à l'instanciation de ladite classe. Cela permet que seules les instances aient des méthodes qui soient des TailCallers. La conversion se faisant au premier appel de la fonction dans l'instance. """ f.tailCaller = True return f class TailCall(object) : """ Appel tail récursif à une fonction et ses argument On peut voir un TailCall comme le fait de retarder l'appel à une fonction (avec ses listes d'arguements) à un moment futur. La fonction sera appelée dans les cas suivant : * Le TailCall est retourné dans l'appel de la fonction d'un TailCaller. * L'exception Continue(TailCall(...)) est levée et traverse une fonction qui est un TailCaller """ def __init__(self, call, *args, **kwargs) : """Création du TailCall, on note la position dans la pile Si la fonction passée à TailCall est un TailCall, on override le TailCall qu'on est en train d'instancier. """ self.stacklvl = len(inspect.stack()) if isinstance(call, TailCall): call.kwargs.update(**kwargs) kwargs = call.kwargs args = call.args + args call = call.call self.call = call self.args = args self.kwargs = kwargs self.check(self.args, self.kwargs) def check(self, args, kwargs): """On vérifie si le TailCall a le bon nombre d'arguments, et si les keywords arguments sont bons.""" call = self.call if isinstance(call, TailCaller): call = call.f targs = inspect.getargspec(call) if targs.varargs is not None: if len(args) + len(kwargs) > len(targs.args): raise TypeError("%s() takes at most %s arguments (%s given)" % (call.func_name, len(targs.args), len(args) + len(kwargs))) for key in kwargs: if key not in targs.args: raise TypeError("%s() got an unexpected keyword argument '%s'" % (call.func_name, key)) def __str__(self): return "TailCall<%s(%s%s%s)>" % ( self.call.func_name, ', '.join(repr(a) for a in self.args), ', ' if self.args and self.kwargs else '', ', '.join("%s=%s" % (repr(k),repr(v)) for (k,v) in self.kwargs.items()) ) def copy(self): ''' Renvois une copie de l'objet courant attention les elements et args ou kwargs sont juste linké ça n'est pas gennant dans la mesure où ils ne sont normalement pas éditer mais remplacé par d'autres éléments ''' result = TailCall(self.call, *list(self.args), **dict(self.kwargs)) result.stacklvl = self.stacklvl return result def __call__(self, *args, **kwargs): """Met à jour le TailCall en ajoutant args et kwargs à la liste des arguments à passer au call lors de son appel via handle. """ tmpkwargs = {} tmpkwargs.update(self.kwargs) tmpkwargs.update(kwargs) self.check(self.args + args, tmpkwargs) self.kwargs.update(kwargs) self.args = self.args + args return self def handle(self) : """ Exécute la fonction call sur sa liste d'argument. Si la fonction à exécuter est un TailCaller, on déréférence. """ call = self.call while isinstance(call, TailCaller): call = call.f return call(*self.args, **self.kwargs) def unicode_of_Error(x): """Formatte des exception""" return u"\n".join(unicode(i, 'utf-8') if type(i) == str else repr(i) for i in x.args) def raiseKeyboardInterrupt(x, y): """fonction utilisée pour réactiver les Ctrl-C""" raise KeyboardInterrupt() class Dialog(object): """Interface de gestion des machines et des adhérents du crans, version lc_ldap""" def __getattribute__(self, attr): ret = super(Dialog, self).__getattribute__(attr) # Petit hack pour que ça soit les methodes de l'objet instancié qui soient # décorée par TailCaller et pas les méthodes statiques de la classe Dialog if getattr(ret, 'tailCaller', False) and not isinstance(ret, TailCaller): ret = TailCaller(ret) ret.__doc__ = ret.f.__doc__ setattr(self, attr, ret) return ret def __init__(self, debug_enable=False): signal.signal(signal.SIGINT, signal.SIG_IGN) self.debug_enable = debug_enable # On met un timeout à 10min d'innactivité sur dialog self.timeout = 600 self.error_to_raise = (Continue, DialogError, ldap.SERVER_DOWN) _dialog = None @property def dialog(self): """ Renvois l'objet dialog. """ if self._dialog is None: self._dialog = PythonDialog() self.dialog_last_access = time.time() return self._dialog def nyan(self, cont, *args, **kwargs): """ Nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan """ (lines, cols) = affichage.getTerminalSize() print "\033[48;5;17m" print " "*(lines * cols) cols = int(min(cols/2, 65)) lines = int(lines) -1 cmd = "/usr/bin/nyancat -W %s -H %s" % (cols, lines) os.system(cmd) raise Continue(cont) @tailcaller def handle_dialog(self, cancel_cont, box, *args): """ Gère les fonctions appelant une fênetre dialog : gestion de l'appuie sur Ctrl-C et rattrapage des exception / segfault de dialog. cancel_cont représente l'endroit où retourner si Ctrl-C """ ctrlC=False ret=None try: signal.signal(signal.SIGINT, raiseKeyboardInterrupt) # Ctrl-C ret = box(*args) signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise Continue(cancel_cont) except DialogTerminatedBySignal as e: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C if e[1] == 11: self.dialog.msgbox( "La fenêtre dialog à été fermée par une erreur de segmentation", timeout=self.timeout, title="Erreur rencontrée", width=73, height=10 ) raise Continue(cancel_cont) else: raise except PythonDialogErrorBeforeExecInChildProcess: self.dialog.msgbox( "La fenêtre dialog à été fermée et a retourné le code 127 : \n" +\ " * peut être que dialog n'a put être éxécuté\n" +\ " * peut être qu'il n'y a plus de mémoire disponible\n" +\ " * peut être que le nombre max de descripteur de fichier a été atteins\n" +\ "ou peut être que dialog fait juste du caca.\n" +\ "Quitter l'interface ?", title="Erreur rencontrée", width=73, height=12, defaultno=True, timeout=self.timeout ) raise Continue(cancel_cont) finally: if ret: return ret else: EnvironmentError("Pas de ret ?!? c'est pas possible ") @tailcaller def handle_dialog_result(self, code, output, cancel_cont, error_cont, codes_todo=[]): """ Gère les fonctions traitant les résultat d'appels à dialog. s'occupe de gérer les exceptions, Ctrl-C, propagation de certaine exceptions, l'appuis sur annuler. Le code à exécuté lui ai passé via la liste codes_todo, qui doit contenir une liste de triple : (code de retour dialog, fonction à exécuter, liste des arguements de la fonction) la fonction est appelée sur ses arguements si le code retourné par dialog correspond. codes_todo = [(code, todo, todo_args)] """ # Si on a appuyé sur annulé ou ESC, on s'en va via la continuation donnée en argument if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(cancel_cont) # Sinon, c'est OK else: for (codes, todo, todo_args) in codes_todo: if code in codes: try: signal.signal(signal.SIGINT, raiseKeyboardInterrupt) # Ctrl-C # On effectue ce qu'il y a a faire dans todo ret = todo(*todo_args) signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C return ret # On propage les Continue except self.error_to_raise: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise # En cas d'une autre erreur, on l'affiche et on retourne au menu d'édition except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C self.dialog.msgbox(traceback.format_exc() if self.debug_enable else "%s" % unicode_of_Error(e), timeout=self.timeout, title="Erreur rencontrée", width=73, height=10) raise Continue(error_cont) except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise Continue(cancel_cont) # En cas de code de retour dialog non attendu, on prévient et on retourne au menu d'édition self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, timeout=self.timeout, title="Erreur rencontrée", width=73, height=10) raise Continue(error_cont) @tailcaller def get_comment(self, title, text, cont, init='', force=False): """ Fait entrer à l'utilisateur un commentaire et le retourne. Si force est à True, on oblige le commentaire à être non vide """ (code, output) = self.dialog.inputbox(text=text, title=title, timeout=self.timeout, init=init) retry_cont = TailCall(self.get_comment, title=title, text=text, cont=cont, force=force) def todo(output, force, title, retry_cont): if force and not output: self.dialog.msgbox("Entrée vide, merci d'indiquer quelque chose", timeout=self.timeout, title=title) raise Continue(retry_cont) else: return unicode(output, 'utf-8') return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [output, force, title, retry_cont])] ) @tailcaller def get_password(self, cont, confirm=True, title="Choix d'un mot de passe", **kwargs): """ Affiche une série d'inpuxbox pour faire entrer un mot de passe puis le retourne, si confirm=True, il y a une confirmation du mot de passe de demandée """ def todo(self_cont, cont): (code, pass1) = self.dialog.passwordbox("Entrez un mot de passe", title=title, timeout=self.timeout, **kwargs) if code != self.dialog.DIALOG_OK: raise Continue(cont) elif not pass1: raise ValueError("Mot de pass vide !") if confirm: (code, pass2) = self.dialog.passwordbox("Comfirmer le mot de passe", timeout=self.timeout, title=title, **kwargs) if code != self.dialog.DIALOG_OK: raise Continue(self_cont) if pass1 != pass2: raise ValueError("Les deux mots de passe ne concordent pas") return pass1 self_cont = TailCall(self.get_password, cont=cont) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [self_cont, cont])] ) @tailcaller def get_timestamp(self, title, text, cont, hour=-1, minute=-1, second=-1, day=0, month=0, year=0): """Fait choisir une date et une heure et retourne le tuple (year, month, day, hour, minute, second)""" retry_cont = TailCall(self.get_timestamp, title=title, text=text, cont=cont, hour=hour, minute=minute, second=second, day=day, month=month, year=year) def get_date(day, month, year): (code, output) = self.dialog.calendar(text, day=day, month=month, year=year, timeout=self.timeout, title=title) if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(cont) elif output: (day, month, year) = output return (year, month, day) else: raise EnvironmentError("Pourquoi je n'ai pas de date ?") def get_time(hour, minute, second, day, month, year): (code, output) = self.dialog.timebox(text, timeout=self.timeout, hour=hour, minute=minute, second=second) if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(retry_cont(day=day, month=month, year=year)) elif output: (hour, minute, second) = output return (hour, minute, second) else: raise EnvironmentError("Pourquoi je n'ai pas d'horaire ?") (year, month, day) = get_date(day, month, year) (hour, minute, second) = get_time(hour, minute, second, day, month, year) return (year, month, day) + (hour, minute, second) def confirm(self, text, title, defaultno=False, width=0, height=0): """wrapper autour du widget yesno""" return self.dialog.yesno( text, no_collapse=True, colors=True, no_mouse=True, timeout=self.timeout, title=title, defaultno=defaultno, width=width, height=height, backtitle="Appuyez sur MAJ pour selectionner du texte" ) == self.dialog.DIALOG_OK