Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/branches/cherrypy-3.0.x/cherrypy/_cpengine.py

Revision 1702 (checked in by fumanchu, 1 year ago)

3.0.x fix: test_states was failing because engine.restart was calling signal.signal in a thread other than the main thread.

  • Property svn:eol-style set to native
Line 
1 """Create and manage the CherryPy application engine."""
2
3 import cgi
4 import os
5 import re
6 import signal
7 import sys
8 import threading
9 import time
10
11 import cherrypy
12 from cherrypy import _cprequest
13
14 # Use a flag to indicate the state of the application engine.
15 STOPPED = 0
16 STARTING = None
17 STARTED = 1
18
19
20 class PerpetualTimer(threading._Timer):
21    
22     def run(self):
23         while True:
24             self.finished.wait(self.interval)
25             if self.finished.isSet():
26                 return
27             self.function(*self.args, **self.kwargs)
28
29
30 class Engine(object):
31     """Interface for (HTTP) applications, plus process controls.
32     
33     Servers and gateways should not instantiate Request objects directly.
34     Instead, they should ask an Engine object for a request via the
35     Engine.request method.
36     
37     Blocking is completely optional! The Engine's blocking, signal and
38     interrupt handling, privilege dropping, and autoreload features are
39     not a good idea when driving CherryPy applications from another
40     deployment tool (but an Engine is a great deployment tool itself).
41     By calling start(blocking=False), you avoid blocking and interrupt-
42     handling issues. By setting Engine.SIGHUP and Engine.SIGTERM to None,
43     you can completely disable the signal handling (and therefore disable
44     autoreloads triggered by SIGHUP). Set Engine.autoreload_on to False
45     to disable autoreload entirely.
46     """
47    
48     # Configurable attributes
49     request_class = _cprequest.Request
50     response_class = _cprequest.Response
51     deadlock_poll_freq = 60
52     autoreload_on = True
53     autoreload_frequency = 1
54     autoreload_match = ".*"
55    
56     def __init__(self):
57         self.state = STOPPED
58        
59         # Startup/shutdown hooks
60         self.on_start_engine_list = []
61         self.on_stop_engine_list = []
62         self.on_start_thread_list = []
63         self.on_stop_thread_list = []
64         self.seen_threads = {}
65        
66         self.servings = []
67        
68         self.mtimes = {}
69         self.reload_files = []
70        
71         self.monitor_thread = None
72    
73     def start(self, blocking=True):
74         """Start the application engine."""
75         self.state = STARTING
76        
77         cherrypy.checker()
78        
79         for func in self.on_start_engine_list:
80             func()
81        
82         self.state = STARTED
83        
84         self._set_signals()
85        
86         freq = self.deadlock_poll_freq
87         if freq > 0:
88             self.monitor_thread = PerpetualTimer(freq, self.monitor)
89             self.monitor_thread.setName("CPEngine Monitor")
90             self.monitor_thread.start()
91        
92         if blocking:
93             self.block()
94    
95     def block(self):
96         """Block forever (wait for stop(), KeyboardInterrupt or SystemExit)."""
97         try:
98             while self.state != STOPPED:
99                 # Note that autoreload_frequency controls
100                 # sleep timer even if autoreload is off.
101                 time.sleep(self.autoreload_frequency)
102                 if self.autoreload_on:
103                     self.autoreload()
104         except KeyboardInterrupt:
105             cherrypy.log("<Ctrl-C> hit: shutting down app engine", "ENGINE")
106             cherrypy.server.stop()
107             self.stop()
108         except SystemExit:
109             cherrypy.log("SystemExit raised: shutting down app engine", "ENGINE")
110             cherrypy.server.stop()
111             self.stop()
112             raise
113         except:
114             # Don't bother logging, since we're going to re-raise.
115             # Note that we don't stop the HTTP server here.
116             self.stop()
117             raise
118    
119     def reexec(self):
120         """Re-execute the current process."""
121         cherrypy.server.stop()
122         self.stop()
123        
124         args = sys.argv[:]
125         cherrypy.log("Re-spawning %s" % " ".join(args), "ENGINE")
126         args.insert(0, sys.executable)
127        
128         if sys.platform == "win32":
129             args = ['"%s"' % arg for arg in args]
130        
131         # Some platforms (OS X) will error if all threads are not
132         # ABSOLUTELY terminated. See http://www.cherrypy.org/ticket/581.
133         for trial in xrange(self.reexec_retry * 10):
134             try:
135                 os.execv(sys.executable, args)
136                 return
137             except OSError, x:
138                 if x.errno != 45:
139                     raise
140                 time.sleep(0.1)
141         else:
142             raise
143    
144     # Number of seconds to retry reexec if os.execv fails.
145     reexec_retry = 2
146    
147     def autoreload(self):
148         """Reload the process if registered files have been modified."""
149         sysfiles = []
150         for k, m in sys.modules.items():
151             if re.match(self.autoreload_match, k):
152                 if hasattr(m, "__loader__"):
153                     if hasattr(m.__loader__, "archive"):
154                         k = m.__loader__.archive
155                 k = getattr(m, "__file__", None)
156                 sysfiles.append(k)
157        
158         for filename in sysfiles + self.reload_files:
159             if filename:
160                 if filename.endswith(".pyc"):
161                     filename = filename[:-1]
162                
163                 oldtime = self.mtimes.get(filename, 0)
164                 if oldtime is None:
165                     # Module with no .py file. Skip it.
166                     continue
167                
168                 try:
169                     mtime = os.stat(filename).st_mtime
170                 except OSError:
171                     # Either a module with no .py file, or it's been deleted.
172                     mtime = None
173                
174                 if filename not in self.mtimes:
175                     # If a module has no .py file, this will be None.
176                     self.mtimes[filename] = mtime
177                 else:
178                     if mtime is None or mtime > oldtime:
179                         # The file has been deleted or modified.
180                         self.reexec()
181    
182     def stop(self):
183         """Stop the application engine."""
184         if self.state != STOPPED:
185             for thread_ident, i in self.seen_threads.iteritems():
186                 for func in self.on_stop_thread_list:
187                     func(i)
188             self.seen_threads.clear()
189            
190             for func in self.on_stop_engine_list:
191                 func()
192            
193             if self.monitor_thread:
194                 self.monitor_thread.cancel()
195                 self.monitor_thread.join()
196                 self.monitor_thread = None
197            
198             self.state = STOPPED
199             cherrypy.log("CherryPy shut down", "ENGINE")
200    
201     def restart(self):
202         """Restart the application engine (does not block)."""
203         self.stop()
204         self.start(blocking=False)
205    
206     def wait(self):
207         """Block the caller until ready to receive requests (or error)."""
208         while not (self.state == STARTED):
209             time.sleep(.1)
210    
211     def request(self, local_host, remote_host, scheme="http",
212                 server_protocol="HTTP/1.1"):
213         """Obtain and return an HTTP Request object. (Core)
214         
215         local_host should be an http.Host object with the server info.
216         remote_host should be an http.Host object with the client info.
217         scheme: either "http" or "https"; defaults to "http"
218         """
219         if self.state == STOPPED:
220             req = NotReadyRequest("The CherryPy engine has stopped.")
221         elif self.state == STARTING:
222             req = NotReadyRequest("The CherryPy engine could not start.")
223         else:
224             # Only run on_start_thread_list if the engine is running.
225             threadID = threading._get_ident()
226             if threadID not in self.seen_threads:
227                 i = len(self.seen_threads) + 1
228                 self.seen_threads[threadID] = i
229                
230                 for func in self.on_start_thread_list:
231                     func(i)
232             req = self.request_class(local_host, remote_host, scheme,
233                                      server_protocol)
234         resp = self.response_class()
235         cherrypy.serving.load(req, resp)
236         self.servings.append((req, resp))
237         return req
238    
239     def release(self):
240         """Close and de-reference the current request and response. (Core)"""
241         req = cherrypy.serving.request
242        
243         try:
244             req.close()
245         except:
246             cherrypy.log(traceback=True)
247        
248         try:
249             self.servings.remove((req, cherrypy.serving.response))
250         except ValueError:
251             pass
252        
253         cherrypy.serving.clear()
254    
255     def monitor(self):
256         """Check timeout on all responses. (Internal)"""
257         if self.state == STARTED:
258             for req, resp in self.servings:
259                 resp.check_timeout()
260    
261     def start_with_callback(self, func, args=None, kwargs=None):
262         """Start the given func in a new thread, then start self and block."""
263        
264         if args is None:
265             args = ()
266         if kwargs is None:
267             kwargs = {}
268         args = (func,) + args
269        
270         def _callback(func, *a, **kw):
271             self.wait()
272             func(*a, **kw)
273         t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
274         t.setName("CPEngine Callback " + t.getName())
275         t.start()
276        
277         self.start()
278    
279    
280     #                           Signal handling                           #
281    
282     SIGHUP = None
283     SIGTERM = None
284    
285     if hasattr(signal, "SIGHUP"):
286         def SIGHUP(self, signum=None, frame=None):
287             self.reexec()
288    
289     if hasattr(signal, "SIGTERM"):
290         def SIGTERM(self, signum=None, frame=None):
291             cherrypy.server.stop()
292             self.stop()
293    
294     def _set_signals(self):
295         try:
296             if self.SIGHUP:
297                 signal.signal(signal.SIGHUP, self.SIGHUP)
298             if self.SIGTERM:
299                 signal.signal(signal.SIGTERM, self.SIGTERM)
300         except ValueError, x:
301             if x.args[0] != 'signal only works in main thread':
302                 raise
303    
304     #                           Drop privileges                           #
305    
306     # Special thanks to Gavin Baker: http://antonym.org/node/100.
307     try:
308         import pwd, grp
309     except ImportError:
310         try:
311             os.umask
312         except AttributeError:
313             def drop_privileges(self):
314                 """Drop privileges. Not implemented on this platform."""
315                 raise NotImplementedError
316         else:
317             umask = None
318            
319             def drop_privileges(self):
320                 """Drop privileges. Windows version (umask only)."""
321                 if self.umask is not None:
322                     old_umask = os.umask(self.umask)
323                     cherrypy.log('umask old: %03o, new: %03o' %
324                                  (old_umask, self.umask), "PRIV")
325     else:
326         uid = None
327         gid = None
328         umask = None
329        
330         def drop_privileges(self):
331             """Drop privileges. UNIX version (uid, gid, and umask)."""
332             if not (self.uid is None and self.gid is None):
333                 if self.uid is None:
334                     uid = None
335                 elif isinstance(self.uid, basestring):
336                     uid = self.pwd.getpwnam(self.uid)[2]
337                 else:
338                     uid = self.uid
339                
340                 if self.gid is None:
341                     gid = None
342                 elif isinstance(self.gid, basestring):
343                     gid = self.grp.getgrnam(self.gid)[2]
344                 else:
345                     gid = self.gid
346                
347                 def names():
348                     name = self.pwd.getpwuid(os.getuid())[0]
349                     group = self.grp.getgrgid(os.getgid())[0]
350                     return name, group
351                
352                 cherrypy.log('Started as %r/%r' % names(), "PRIV")
353                 if gid is not None:
354                     os.setgid(gid)
355                 if uid is not None:
356                     os.setuid(uid)
357                 cherrypy.log('Running as %r/%r' % names(), "PRIV")
358            
359             if self.umask is not None:
360                 old_umask = os.umask(self.umask)
361                 cherrypy.log('umask old: %03o, new: %03o' %
362                              (old_umask, self.umask), "PRIV")
363
364
365 class NotReadyRequest:
366    
367     throw_errors = True
368     show_tracebacks = True
369     error_page = {}
370    
371     def __init__(self, msg):
372         self.msg = msg
373         self.protocol = (1,1)
374    
375     def close(self):
376         pass
377    
378     def run(self, method, path, query_string, protocol, headers, rfile):
379         self.method = "GET"
380         cherrypy.HTTPError(503, self.msg).set_response()
381         cherrypy.response.finalize()
382         return cherrypy.response
383
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets