cubicweb/pyramid/pyramidctl.py
changeset 12730 6c48a49cd3c2
parent 12729 c8ca784fdd77
child 12736 5add82b08a6d
equal deleted inserted replaced
12729:c8ca784fdd77 12730:6c48a49cd3c2
    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: