Layer 1

mardi 26 février 2013

Limiter un process dans le temps

Introduction:
Il peut être utile de limiter une tache ou l'utilisation d'un exécutable dans le temps: limiter le temps d'utilisation d'un jeu pour un enfant (ou un adulte ;-), limiter l'utilisation d'un programme suivant un abonnement ou un crédit de temps...
Après des tests d’accès aux ressources windows via la librairie WMI, puis l'utilisation de multiples de threads, j'ai finalement utilisé des utilitaires de base de windows comme tasklist et taskkill pour accéder aux processus windows: simple et efficace...

Solution:
LimitTask.py se lance comme une commande avec 3 paramètres:  le premier donne le chemin complet de l'exécutable à surveiller, le second donne le temps autorisé d'utilisation de cet exécutable et enfin le troisième paramètre permet la remise à zéro  régulière de ce décompte de temps d'utilisation (abonnement).

L'algorithme est décrit ci-dessous:


Remarques:
-L'utilisation d'une sauvegarde régulière des paramètres de temps, via le fichier LTsave.txt, est indispensable si, par exemple, on redémarre son PC. La relance de LimitTask permet de ne pas perdre la référence du temps d'utilisation/jeu déjà entamés.
-L'utilisation de boite de dialog éphémère (MessageBoxTimeout dans la class LimitTask) est une façon simple de signaler une action (ici le 'kill' de l'exécutable) sans bloquer l’application principale
-Pas d'utilisation de librairies complexes comme WX ou QT, mais uniquement les ressources de bases IHM/GUI Windows via l'excellent wrapper Python for Windows extensions (PyWin32). On aurait pu directement utiliser la librairie windll.user32 via ctypes (moins pythonic)

Vous pouvez retrouvez le code via GitHub.

Dans la suite du billet, le code LimitTask.py et quelques informations pour créer une application windows avec le module py2exe.



  1 #!/usr/bin/python
  2 # -*- coding: utf-8 -*-
  3 
  4 """
  5 LimitTask module is a simple command to limit one specific windows processus (task) for a certain time during a slot of time (subscription).
  6 LimitTask est un module python pour limiter l'usage d'un process windows dans le temps sur une plage de temps déterminé (abonnement).
  7 
  8 Usage = 
  9           python.exe LimitTask.pyw <name_process/task.exe> <max use time> <abonnement/plage de temps>
 10 
 11 @author: Python4D/damien.samain@python4d.com
 12 @version: 0.1 bêta - Utilisation du module wmi - Log via module logging fichier LMLog
 13 @version: 0.2 bêta - Abandon du module wmi utilisation des commandes windows tasklist et taskkill - Sauvegarde PlageTime et TaskTime dans un fichier text LTsave.txt
 14 @version: 0.3 bêta - Création de boite de dialogue éphémère - abandon de logging
 15 @todo: minimum gui, password, save data in registery
 16 """
 17 
 18 import time,sys,os,subprocess
 19 # Importation des constantes windows (win32con) et wrapper of win32's functions 
 20 #@note: http://sourceforge.net/projects/pywin32/
 21 import win32gui,win32con
 22 
 23 
 24 class LimitTask(object):
 25   
 26   def LimitTask(self,Task,NbMin=60,Abonnement=60*24):
 27     
 28     def writesavefile():
 29       with open("LTsave.txt",'w') as f:
 30         f.writelines([str(PlageTime),"\n",str(TaskTime)])     
 31         
 32     TaskTime,PlageTime=[0,time.time()]
 33     if os.path.isfile("LTsave.txt"):
 34       state=15
 35     else:
 36       state=10
 37 
 38     #TODO: il faudrait reprendre la state machine pour incorporer les deux boucles while en une seule "while true"
 39     #TODO: on a séparé la state machine qui controle le temps de tache avec celle de la plage 
 40     while True: #Boucle infinie gérant la vérification de la plage et tasktime
 41       print "WHILE TRUE => state={:d}, tasktime={:4.1f}, plagetime={:4.1f}".format(state,TaskTime,time.time()-PlageTime)
 42       while TaskTime<NbMin*60: #boucle (statemachine) gérant le temps d'entrée et sortie dans la tache
 43         print "WHILE TaskTime<NbMin*60 => state={:d}, tasktime={:4.1f}, plagetime={:4.1f}".format(state,TaskTime,time.time()-PlageTime)
 44         if state==10: # init des flags et Temps
 45           self.flag=0 #flag de la tache dThread pour vérifier si on est sortie de la tache
 46           self.flag_message=0
 47           TaskTime=0  
 48           PlageTime=time.time()
 49           LastTaskTime=0
 50           state=20
 51         if state==15: # Récupération des infos du fichier de sauvegarde des données PlgeTime et TaskTime
 52           self.flag=0 #flag de la tache dThread pour vérifier si on est sortie de la tache
 53           self.flag_message=0
 54           with open("LTsave.txt",'r') as f:
 55             PlageTime,TaskTime=map(float,f.readlines())         
 56           LastTaskTime=TaskTime
 57           state=20
 58         elif state==20:
 59           process=self.FindPID(Task)
 60           if process==[]:    
 61             writesavefile()
 62             time.sleep(1)
 63             state=20
 64           else:          
 65             if PlageTime+Abonnement*60<time.time():
 66               print u"Remise à zéro au début Task !"            
 67               state=10 #On reset tout
 68             else:
 69               print u"Task trouvé! Id={}-Name={}".format(process[0][0],process[0][1])
 70               start=time.time()
 71               state=50
 72         elif state==50:#task présent reprise du calcul du temps
 73           process=self.FindPID(process[0][0])
 74           if process==[]:
 75             self.flag=0
 76             print(u"Sortie de {} - Temps total de jeu = {:4.1f} minutes- Temps avant remise à zéro ={:4.1f} minute(s)!".format(Task,TaskTime/60.0,(Abonnement*60+PlageTime-time.time())/60.0)) 
 77             LastTaskTime=TaskTime #récupération du temps déjà écoulé
 78             state=20 #on retourne vérifier qu'il n'y a plus de process en cours
 79           else:
 80             writesavefile()
 81             time.sleep(1)
 82             TaskTime=LastTaskTime+time.time()-start
 83             if PlageTime+Abonnement*60<time.time():
 84               print u"Remise à zéro avant la fin de TaskTime !"
 85               state=10 #On reset tout
 86 #fin de While TaskTime<NbMin*60 => le temps limite et dépassé  
 87       writesavefile()
 88       time.sleep(1)  
 89       process=self.FindPID(Task)
 90       if process==[]: #pas de process en cours
 91         if PlageTime+Abonnement*60<time.time():
 92           print u"Remise à zéro en dehors du jeu!\n"
 93           TaskTime=0  #permet de rerentrer dans la state machine 
 94           state=10
 95       else:
 96         if not self.flag_message==2:
 97           self.MessageBoxTimeout(0, u"Tu dois quitter le jeu maintenant - Tu as dépassé les {:4.1f} minute(s) !".format(NbMin), "LimitTask - QUOTA DEPASSE !",4096,10)
 98           subprocess.check_output('taskkill /F /PID '+str(process[0][1]))
 99           self.flag_message=2
100         else:
101           subprocess.check_output('taskkill /F /PID '+str(process[0][1]))
102           self.MessageBoxTimeout(0, u"Tu as dépassé tes {:4.0f} minute(s) de jeu,\n tu dois attendre encore {:4.1f} minute(s) pour rejouer !".format(NbMin,(Abonnement*60+PlageTime-time.time())/60.0), "LimitTask - QUOTA DEPASSE !", 4096,10)          
103 
104   def FindPID(self,exename):
105       p=subprocess.Popen('tasklist /FI "IMAGENAME eq '+exename+'"',stdout=subprocess.PIPE,stderr=subprocess.PIPE,creationflags=subprocess.SW_HIDE,shell=True)
106       a=p.stdout.readlines()
107       info=[]
108       i=0
109       thispid=str(os.getpid())
110       while len(a)>3+i and a[3+i].split()!=[]:
111         info.append(a[3+i].split())
112         i+=1
113       info=filter(lambda i:i[1]!=thispid,info)
114       return (info)
115   def MessageBoxTimeout(self,parent,title,message,options=win32con.MB_SYSTEMMODAL,timeout=10):
116     """
117     Création d'une boite de dialogue (#32770 class windows)
118     Attente d'un timeout
119     Fermeture de la boite de dialogue
120     """
121     from threading import Thread as Process
122     _p=Process(target=win32gui.MessageBox, args=(parent,title,message,options))
123     _p.start()
124     time.sleep(timeout)
125     hwnd=win32gui.FindWindow(32770,u"LimitTask - QUOTA DEPASSE !")
126     win32gui.PostMessage(hwnd,win32con.WM_CLOSE)
127   
128 if __name__=="__main__":
129   if len(sys.argv)!=4:
130     win32gui.MessageBox(0,
131 u"""Attention ! il y 3 arguments à cette commande:
132 
133         1) 'nom de la tache' task présente dans la liste des processus windows
134         2) Temps utilisation max. de l'exécutable - tps de jeu (minutes)
135         3) Temps du renouvelement de l'abonnement - tps remise à zéro (minutes)
136         
137     Exemple (pour limiter la l'utilisation de la calculatrice à 10 mn toutes les 60mn):
138     
139                >>{} calc.exe 10 60""".format(sys.argv[0].split(os.path.sep)[-1:][0][:-4]), u"Erreur de lancement !",4096) #récupération du dernier élément du fullpath [-1:] d'une liste [0], sans les 4 derniers caractères
140   elif int(sys.argv[2])>=int(sys.argv[3]) : 
141       win32gui.MessageBox(0, u"Attention la Plage de remise à zéro doit être Supérieure au temps de la tache/jeu!", u"LimitTask - Erreur de lancement !",4096)
142   else:
143     #TODO: vérifier qu'un autre LimitTask.exe n'existe pas avec la même tache
144     MyPC=LimitTask()
145     while True:  
146       process=MyPC.FindPID(os.path.split(sys.argv[0])[1].split('.')[0]+'.exe')
147       if process==[]: break
148       subprocess.check_output('taskkill /F /PID '+str(process[0][1]))   
149     MyPC.LimitTask(sys.argv[1],int(sys.argv[2]),int(sys.argv[3]))
150     

Pour créer une application autonome:
On utilise le module et script py2exe qui va permettre de créer une application (.exe). Module non standard, il faut donc l'installer.
Pour lancer la création de la distribution [c-à-d, l'exécutable+librairies windows], on va créer un fichier python qui utilisera la méthode setup du module distutils. Le module distutils est un module standard pour l'installation des modules python et création d'installeur windows mais peut-être aussi utiliser pour lancer un script particulier comme py2exe; on utilise donc cette méthode "setup" du module distutils 'uniquement' pour lancer notre script py2exe et créer un exécutable windows à partir de notre fichier python LimitTask.py.


#Fichier de setup pour LimitTask - LimitTaskSetup.py 
from distutils.core import setup 
import py2exesetup(windows=['LimitTask.py'], options = {"py2exe": {"compressed": 1, "bundle_files": 1, } }) 

L'argument principal de la méthode setup est "windows=": il indique que l'on veut utiliser une application de type windows plutot que du type "console=" afin de ne pas voir la console python apparaître au lancement de notre application.
L'utilisation des options "compressed" et "bundles_files" permet de ne créer qu'un fichier zip utile pour notre exécutable, où sera rassemblé tous les fichiers nécessaires dont la dll de l'interpréteur python: python27.dll.


Pour obtenir l'exécutable il suffit donc de lancer ce fichier de setup, LimitTaskSetup.py, avec comme argument le script py2exe.

python LimitTaskSetup.py py2exe


Après l'exécution de la commande ci dessus, un directory dist sera créé avec trois fichiers: l'exécutable LimitTask.exe, library.zip et w9xopen.exe. Ce dernier fichier n'est pas utile si vous utilisez windows xp ou supérieur.

Afin de passer les arguments à notre exécutable LimitTask.exe, créez un raccourci.



Vous pouvez alors mettre ce raccourci dans les taches à exécuter au démarrage windows.


Aucun commentaire:

Enregistrer un commentaire