scripts/gestion/dialog/CPS.py
2015-03-04 00:01:10 +01:00

477 lines
20 KiB
Python

#!/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