23 |
23 |
24 The reloading strategy is heavily inspired by (and partially copied from) |
24 The reloading strategy is heavily inspired by (and partially copied from) |
25 the pyramid script 'pserve'. |
25 the pyramid script 'pserve'. |
26 """ |
26 """ |
27 |
27 |
28 import atexit |
|
29 import errno |
|
30 import os |
28 import os |
31 import signal |
29 import signal |
32 import sys |
30 import sys |
33 import time |
31 import time |
34 import threading |
32 import threading |
35 import subprocess |
33 import subprocess |
36 |
34 |
37 from cubicweb import ExecutionError |
|
38 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg |
35 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg |
39 from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold |
36 from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold |
40 from cubicweb.pyramid import wsgi_application_from_cwconfig |
37 from cubicweb.pyramid import wsgi_application_from_cwconfig |
41 from cubicweb.pyramid.config import get_random_secret_key |
38 from cubicweb.pyramid.config import get_random_secret_key |
42 from cubicweb.server import serverctl, set_debug |
39 from cubicweb.server import serverctl, set_debug |
95 """ |
92 """ |
96 name = 'pyramid' |
93 name = 'pyramid' |
97 actionverb = 'started' |
94 actionverb = 'started' |
98 |
95 |
99 options = ( |
96 options = ( |
100 ('no-daemon', |
|
101 {'action': 'store_true', |
|
102 'help': 'Run the server in the foreground.'}), |
|
103 ('debug-mode', |
97 ('debug-mode', |
104 {'action': 'store_true', |
98 {'action': 'store_true', |
105 'help': 'Activate the repository debug mode (' |
99 'help': 'Activate the repository debug mode (' |
106 'logs in the console and the debug toolbar).' |
100 'logs in the console and the debug toolbar).'}), |
107 ' Implies --no-daemon'}), |
|
108 ('debug', |
101 ('debug', |
109 {'short': 'D', 'action': 'store_true', |
102 {'short': 'D', 'action': 'store_true', |
110 'help': 'Equals to "--debug-mode --no-daemon --reload"'}), |
103 'help': 'Equals to "--debug-mode --reload"'}), |
111 ('reload', |
104 ('reload', |
112 {'action': 'store_true', |
105 {'action': 'store_true', |
113 'help': 'Restart the server if any source file is changed'}), |
106 'help': 'Restart the server if any source file is changed'}), |
114 ('reload-interval', |
107 ('reload-interval', |
115 {'type': 'int', 'default': 1, |
108 {'type': 'int', 'default': 1, |
175 "handle this issue you must have the win32api module " |
168 "handle this issue you must have the win32api module " |
176 "installed" % arg) |
169 "installed" % arg) |
177 arg = win32api.GetShortPathName(arg) |
170 arg = win32api.GetShortPathName(arg) |
178 return arg |
171 return arg |
179 |
172 |
180 def _remove_pid_file(self, written_pid, filename): |
|
181 current_pid = os.getpid() |
|
182 if written_pid != current_pid: |
|
183 # A forked process must be exiting, not the process that |
|
184 # wrote the PID file |
|
185 return |
|
186 if not os.path.exists(filename): |
|
187 return |
|
188 with open(filename) as f: |
|
189 content = f.read().strip() |
|
190 try: |
|
191 pid_in_file = int(content) |
|
192 except ValueError: |
|
193 pass |
|
194 else: |
|
195 if pid_in_file != current_pid: |
|
196 msg = "PID file %s contains %s, not expected PID %s" |
|
197 self.out(msg % (filename, pid_in_file, current_pid)) |
|
198 return |
|
199 self.info("Removing PID file %s" % filename) |
|
200 try: |
|
201 os.unlink(filename) |
|
202 return |
|
203 except OSError as e: |
|
204 # Record, but don't give traceback |
|
205 self.out("Cannot remove PID file: (%s)" % e) |
|
206 # well, at least lets not leave the invalid PID around... |
|
207 try: |
|
208 with open(filename, 'w') as f: |
|
209 f.write('') |
|
210 except OSError as e: |
|
211 self.out('Stale PID left in file: %s (%s)' % (filename, e)) |
|
212 else: |
|
213 self.out('Stale PID removed') |
|
214 |
|
215 def record_pid(self, pid_file): |
|
216 pid = os.getpid() |
|
217 self.debug('Writing PID %s to %s' % (pid, pid_file)) |
|
218 with open(pid_file, 'w') as f: |
|
219 f.write(str(pid)) |
|
220 atexit.register( |
|
221 self._remove_pid_file, pid, pid_file) |
|
222 |
|
223 def daemonize(self, pid_file): |
|
224 pid = live_pidfile(pid_file) |
|
225 if pid: |
|
226 raise ExecutionError( |
|
227 "Daemon is already running (PID: %s from PID file %s)" |
|
228 % (pid, pid_file)) |
|
229 |
|
230 self.debug('Entering daemon mode') |
|
231 pid = os.fork() |
|
232 if pid: |
|
233 # The forked process also has a handle on resources, so we |
|
234 # *don't* want proper termination of the process, we just |
|
235 # want to exit quick (which os._exit() does) |
|
236 os._exit(0) |
|
237 # Make this the session leader |
|
238 os.setsid() |
|
239 # Fork again for good measure! |
|
240 pid = os.fork() |
|
241 if pid: |
|
242 os._exit(0) |
|
243 |
|
244 # @@: Should we set the umask and cwd now? |
|
245 |
|
246 import resource # Resource usage information. |
|
247 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] |
|
248 if (maxfd == resource.RLIM_INFINITY): |
|
249 maxfd = MAXFD |
|
250 # Iterate through and close all file descriptors. |
|
251 for fd in range(0, maxfd): |
|
252 try: |
|
253 os.close(fd) |
|
254 except OSError: # ERROR, fd wasn't open to begin with (ignored) |
|
255 pass |
|
256 |
|
257 if (hasattr(os, "devnull")): |
|
258 REDIRECT_TO = os.devnull |
|
259 else: |
|
260 REDIRECT_TO = "/dev/null" |
|
261 os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) |
|
262 # Duplicate standard input to standard output and standard error. |
|
263 os.dup2(0, 1) # standard output (1) |
|
264 os.dup2(0, 2) # standard error (2) |
|
265 |
|
266 def restart_with_reloader(self, filelist_path): |
173 def restart_with_reloader(self, filelist_path): |
267 self.debug('Starting subprocess with file monitor') |
174 self.debug('Starting subprocess with file monitor') |
268 |
175 |
269 # Create or clear monitored files list file. |
176 # Create or clear monitored files list file. |
270 with open(filelist_path, 'w') as f: |
177 with open(filelist_path, 'w') as f: |
334 yield f |
241 yield f |
335 |
242 |
336 def pyramid_instance(self, appid): |
243 def pyramid_instance(self, appid): |
337 self._needreload = False |
244 self._needreload = False |
338 |
245 |
339 debugmode = self['debug-mode'] or self['debug'] |
|
340 autoreload = self['reload'] or self['debug'] |
246 autoreload = self['reload'] or self['debug'] |
341 daemonize = not (self['no-daemon'] or debugmode or autoreload) |
247 |
342 |
248 # debugmode=True is to force to have a StreamHandler used instead of |
343 cwconfig = cwcfg.config_for(appid, debugmode=debugmode) |
249 # writting the logs into a file in /tmp |
|
250 cwconfig = cwcfg.config_for(appid, debugmode=True) |
344 filelist_path = os.path.join(cwconfig.apphome, |
251 filelist_path = os.path.join(cwconfig.apphome, |
345 '.pyramid-reload-files.list') |
252 '.pyramid-reload-files.list') |
346 |
253 |
347 pyramid_ini_path = os.path.join(cwconfig.apphome, "pyramid.ini") |
254 pyramid_ini_path = os.path.join(cwconfig.apphome, "pyramid.ini") |
348 if not os.path.exists(pyramid_ini_path): |
255 if not os.path.exists(pyramid_ini_path): |
358 extra_files.extend(self.configfiles(cwconfig)) |
265 extra_files.extend(self.configfiles(cwconfig)) |
359 extra_files.extend(self.i18nfiles(cwconfig)) |
266 extra_files.extend(self.i18nfiles(cwconfig)) |
360 self.install_reloader( |
267 self.install_reloader( |
361 self['reload-interval'], extra_files, |
268 self['reload-interval'], extra_files, |
362 filelist_path=filelist_path) |
269 filelist_path=filelist_path) |
363 |
|
364 if daemonize: |
|
365 self.daemonize(cwconfig['pid-file']) |
|
366 self.record_pid(cwconfig['pid-file']) |
|
367 |
270 |
368 if self['dbglevel']: |
271 if self['dbglevel']: |
369 self['loglevel'] = 'debug' |
272 self['loglevel'] = 'debug' |
370 set_debug('|'.join('DBG_' + x.upper() for x in self['dbglevel'])) |
273 set_debug('|'.join('DBG_' + x.upper() for x in self['dbglevel'])) |
371 init_cmdline_log_threshold(cwconfig, self['loglevel']) |
274 init_cmdline_log_threshold(cwconfig, self['loglevel']) |
390 return 3 |
293 return 3 |
391 return 0 |
294 return 0 |
392 |
295 |
393 |
296 |
394 CWCTL.register(PyramidStartHandler) |
297 CWCTL.register(PyramidStartHandler) |
395 |
|
396 |
|
397 def live_pidfile(pidfile): # pragma: no cover |
|
398 """(pidfile:str) -> int | None |
|
399 Returns an int found in the named file, if there is one, |
|
400 and if there is a running process with that process id. |
|
401 Return None if no such process exists. |
|
402 """ |
|
403 pid = read_pidfile(pidfile) |
|
404 if pid: |
|
405 try: |
|
406 os.kill(int(pid), 0) |
|
407 return pid |
|
408 except OSError as e: |
|
409 if e.errno == errno.EPERM: |
|
410 return pid |
|
411 return None |
|
412 |
|
413 |
|
414 def read_pidfile(filename): |
|
415 if os.path.exists(filename): |
|
416 try: |
|
417 with open(filename) as f: |
|
418 content = f.read() |
|
419 return int(content.strip()) |
|
420 except (ValueError, IOError): |
|
421 return None |
|
422 else: |
|
423 return None |
|
424 |
298 |
425 |
299 |
426 def _turn_sigterm_into_systemexit(): |
300 def _turn_sigterm_into_systemexit(): |
427 """Attempts to turn a SIGTERM exception into a SystemExit exception.""" |
301 """Attempts to turn a SIGTERM exception into a SystemExit exception.""" |
428 try: |
302 try: |