]> git.llucax.com Git - software/pymin.git/blob - pymin/procman.py
Change ProcessInfo.running to a read-only property.
[software/pymin.git] / pymin / procman.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 import os
4 import errno
5 import signal
6 import subprocess
7 import logging ; log = logging.getLogger('pymin.procman')
8
9 __all__ = ('ProcessManager', 'manager', 'register', 'unregister', 'call',
10            'start', 'stop', 'restart', 'kill', 'get', 'has', 'sigchild_handler')
11
12 class ProcessInfo:
13     def __init__(self, name, command, callback=None, persist=False,
14                  max_errors=3, args=None, kwargs=None):
15         self.name = name
16         self.command = command
17         self.callback = callback
18         if args is None: args = list()
19         self.args = args
20         if kwargs is None: kwargs = dict()
21         self.kwargs = kwargs
22         self.persist = persist
23         self.max_errors = max_errors
24         self.clear()
25     def clear(self):
26         self.dont_run = False
27         self.signal = None
28         self.process = None
29         self.error_count = 0
30     def start(self):
31         assert self.process is None
32         self.restart()
33     def restart(self):
34         self.clear()
35         log.debug(u'ProcessInfo.restart(): executing %s', self.command)
36         self.process = subprocess.Popen(self.command, *self.args, **self.kwargs)
37     def stop(self):
38         assert self.process is not None
39         self.dont_run = True
40         if self.signal == signal.SIGTERM or self.signal == signal.SIGKILL:
41             # Allready stopped, kill it
42             self.kill(signal.SIGKILL)
43         else:
44             # Stop it
45             self.kill(signal.SIGTERM)
46     def kill(self, signum):
47         log.debug(u'ProcessInfo.kill(): killing pid %s with signal %s',
48                       self.process.pid, signum)
49         assert self.process is not None
50         os.kill(self.process.pid, signum)
51         self.signal = signum
52     @property
53     def running(self):
54         return self.process is not None and self.process.poll() is None
55     def __repr__(self):
56         pid = None
57         if self.process is not None:
58             pid = self.process.pid
59         return 'ProcessInfo(name=%s, pid=%s command=%s, persist=%s, cb=%s)' % (
60                     self.name, pid, self.command, self.persist,
61                     self.callback.__name__)
62
63 class ProcessManager:
64
65     def __init__(self):
66         self.services = dict()
67         self.namemap = dict()
68         self.pidmap = dict()
69         log.debug(u'ProcessManager()')
70
71     def register(self, name, command, callback=None, persist=False,
72                 max_errors=3, *args, **kwargs):
73         log.debug(u'ProcessManager.register(%s, %s, %s, %s, %s, %s, %s)',
74                   name, command, callback, persist, max_errors, args, kwargs)
75         self.services[name] = ProcessInfo(name, command, callback, persist,
76                                           max_errors, args, kwargs)
77
78     def unregister(self, name):
79         log.debug(u'ProcessManager.unregister(%s)', name)
80         del self.services[name]
81
82     def _call(self, pi):
83         pi.start()
84         self.namemap[pi.name] = self.pidmap[pi.process.pid] = pi
85
86     def call(self, name, command, callback=None, persist=False,
87                 max_errors=3, *args, **kwargs):
88         log.debug(u'ProcessManager.call(%s, %s, %s, %s, %s, %s, %s)',
89                   name, command, callback, persist, max_errors, args, kwargs)
90         pi = ProcessInfo(name, command, callback, persist, max_errors,
91                          args, kwargs)
92         self._call(pi)
93
94     def start(self, name):
95         log.debug(u'ProcessManager.start(%s)', name)
96         assert name not in self.namemap
97         self._call(self.services[name])
98
99     def stop(self, name):
100         log.debug(u'ProcessManager.stop(%s)', name)
101         assert name in self.namemap
102         self.namemap[name].stop()
103
104     def restart(self, name):
105         log.debug(u'ProcessManager.restart(%s)', name)
106         # we have to check first in namemap in case is an unregistered
107         # process (added with call())
108         if name in self.namemap:
109             pi = self.namemap[name]
110             pi.stop()
111             pi.process.wait()
112             pi.restart()
113         else:
114             self.services[name].start()
115
116     def kill(self, name, signum):
117         log.debug(u'ProcessManager.kill(%s, %s)', name, signum)
118         assert name in self.namemap
119         self.namemap[name].kill(name, stop)
120
121     def sigchild_handler(self, signum, stack_frame=None):
122         log.debug(u'ProcessManager.sigchild_handler(%s)', signum)
123         try:
124             (pid, status) = os.waitpid(-1, os.WNOHANG)
125         except OSError, e:
126             log.debug(u'ProcessManager.sigchild_handler(): OSError')
127             if e.errno is errno.ECHILD:
128                 log.debug(u'ProcessManager.sigchild_handler(): OSError ECHILD')
129                 return
130             raise
131         log.debug(u'ProcessManager.sigchild_handler: pid=%s, status=%s',
132                       pid, status)
133         while pid:
134             if pid in self.pidmap:
135                 p = self.pidmap[pid]
136                 p.process.returncode = status
137                 if p.callback is not None:
138                     log.debug(u'ProcessManager.sigchild_handler: '
139                                   u'calling %s(%s)', p.callback.__name__, p)
140                     p.callback(self, p)
141                 if p.dont_run or not p.persist or p.error_count >= p.max_errors:
142                     log.debug(u"ProcessManager.sigchild_handler: can't "
143                             u'persist, dont_run=%s, persist=%s, error_cout=%s, '
144                             u'max_errors=%s', p.dont_run, p.persist,
145                             p.error_count, p.max_errors)
146                     del self.namemap[p.name]
147                     del self.pidmap[pid]
148                     p.clear()
149                 else:
150                     log.debug(u'ProcessManager.sigchild_handler: persist')
151                     if p.process.returncode == 0:
152                         p.error_count = 0
153                         log.debug(u'ProcessManager.sigchild_handler: '
154                                 u'return OK, resetting error_count')
155                     else:
156                         p.error_count += 1
157                         log.debug(u'ProcessManager.sigchild_handler: return'
158                                 u'not 0, error_count + 1 = %s', p.error_count)
159                     del self.pidmap[pid]
160                     p.restart()
161                     self.pidmap[p.process.pid] = p
162             try:
163                 (pid, status) = os.waitpid(-1, os.WNOHANG)
164             except OSError, e:
165                 if e.errno == errno.ECHILD:
166                     return
167                 raise
168
169     def get(self, name):
170         if isinstance(name, basestring): # is a name
171             if name in self.namemap:
172                 return self.namemap[name]
173             if name in self.services:
174                 return self.services[name]
175         else: # is a pid
176             if name in self.pidmap:
177                 return self.pidmap[name]
178         raise KeyError, name
179
180     def has(self, name):
181         if isinstance(name, basestring): # is a name
182             if name in self.namemap:
183                 return True
184             if name in self.services:
185                 return True
186         else: # is a pid
187             if name in self.pidmap:
188                 return True
189         return False
190
191     def __getitem__(self, name):
192         return self.get(name)
193
194     def __contains__(self, name):
195         return self.has(name)
196
197
198 if __name__ == '__main__':
199     logging.basicConfig(
200         level   = logging.DEBUG,
201         format  = '%(asctime)s %(levelname)-8s %(message)s',
202         datefmt = '%H:%M:%S',
203     )
204
205
206 # Globals
207 manager = ProcessManager()
208 register = manager.register
209 unregister = manager.unregister
210 call = manager.call
211 start = manager.start
212 stop = manager.stop
213 restart = manager.restart
214 kill = manager.kill
215 get = manager.get
216 has = manager.has
217 sigchild_handler = manager.sigchild_handler
218
219
220 if __name__ == '__main__':
221
222     import signal
223     import time
224
225     sig = None
226     count = 0
227
228     def SIGCHLD_handler(signum, stacktrace):
229         global sig
230         sig = signum
231         print 'SIGCHLD', signum
232
233     def notify(pm, pi):
234         global count
235         if pi.name == 'test-service':
236             print 'test-service count =', count
237             count += 1
238             if count > 4:
239                 print 'set test-service non-persistent, start test-service-2'
240                 pi.persist = False
241                 assert 'test-service-2' not in manager.namemap
242                 pm.start('test-service-2')
243                 assert 'test-service-2' in manager.namemap
244                 assert get('test-service-2').running
245         print 'died:', pi.name, pi.command
246
247     register('test-service', ('sleep', '2'), notify, True)
248     assert 'test-service' in manager.services
249     assert 'test-service' not in manager.namemap
250     assert not get('test-service').running
251
252     register('test-service-2', ('sleep', '3'), notify, False)
253     assert 'test-service-2' in manager.services
254     assert 'test-service-2' not in manager.namemap
255     assert not get('test-service-2').running
256
257     signal.signal(signal.SIGCHLD, SIGCHLD_handler)
258
259     call('test-once', ('sleep', '5'), notify)
260     assert 'test-once' not in manager.services
261     assert 'test-once' in manager.namemap
262     assert get('test-once').running
263
264     start('test-service')
265     assert 'test-service' in manager.namemap
266     assert get('test-service').running
267
268     print "Known processes:", manager.services.keys()
269     print "Waiting...", manager.namemap.keys()
270     print "------------------------------------------------------------------"
271     while manager.pidmap:
272         signal.pause()
273         if sig == signal.SIGCHLD:
274             sigchild_handler(sig)
275             sig = None
276         print "Known processes:", manager.services.keys()
277         print "Waiting...", manager.namemap.keys()
278         print "------------------------------------------------------------------"
279     assert 'test-service' not in manager.namemap
280     assert 'test-service-2' not in manager.namemap
281     assert 'test-once' not in manager.services
282     assert 'test-once' not in manager.namemap
283
284     call('test-wait', ('sleep', '2'))
285     print 'test-wait returned?', get('test-wait').process.poll()
286     assert get('test-wait').running
287     print 'Waiting test-wait to return...'
288     ret = get('test-wait').process.wait()
289     print 'Done! returned:', ret
290