Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,57 @@ where specified.

*Introduced*: 3.0

``post_stop_command``

A command that will be executed after the process has stopped or exited.
This command is executed by the shell, so you can use shell features
like pipes, redirection, and command substitution. The command is
executed synchronously (blocking), meaning that :program:`supervisord`
will wait for the command to complete before continuing with other tasks.
The command will be executed after waiting for ``post_stop_command_delay``
seconds. This is useful for cleanup tasks, notifications, or any
post-processing that needs to occur after a process stops.

.. note::

The post-stop command will be executed when the process transitions to
any of the following states:

- STOPPED: as a result of a stop request (manual stop or shutdown)
- EXITED: when the process exits with an expected or unexpected exit code
- BACKOFF: when the process exits too quickly during startup

The post-stop command will not be executed if the process transitions to
other states like STARTING, RUNNING, or UNKNOWN.

.. warning::

The post-stop command has a default timeout of 30 seconds. If the
command takes longer than this, it will be terminated. Design your
post-stop commands to complete quickly to avoid blocking supervisor
operations.

*Default*: None (no command will be executed)

*Required*: No.

*Introduced*: 4.x.x

``post_stop_command_delay``

The number of seconds to wait after the process has stopped or exited before
executing the ``post_stop_command``. This delay occurs after the
process has transitioned to the STOPPED, EXITED, or BACKOFF state. During this delay,
:program:`supervisord` will block and wait before executing the
post-stop command. Set this to 0 to execute the command immediately
after the process stops or exits.

*Default*: 0

*Required*: No.

*Introduced*: 4.x.x

``[program:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -1174,6 +1225,8 @@ where specified.
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
post_stop_command=/path/to/cleanup.sh
post_stop_command_delay=5

``[include]`` Section Settings
------------------------------
Expand Down Expand Up @@ -1443,6 +1496,8 @@ above constraints and additions.
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
post_stop_command=/path/to/cleanup.sh
post_stop_command_delay=0

``[eventlistener:x]`` Section Settings
--------------------------------------
Expand Down Expand Up @@ -1525,6 +1580,8 @@ above constraints and additions.
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
post_stop_command=/path/to/cleanup.sh
post_stop_command_delay=0

``[rpcinterface:x]`` Section Settings
-------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
author="Chris McDonough",
author_email="chrism@plope.com",
packages=find_packages(),
install_requires=[],
install_requires=['subprocess32'] if py_version[0] == 2 else [],
extras_require={
'test': ['pytest', 'pytest-cov']
},
Expand Down
14 changes: 12 additions & 2 deletions supervisor/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,8 @@ def get(section, opt, *args, **kwargs):
stopasgroup = boolean(get(section, 'stopasgroup', 'false'))
killasgroup = boolean(get(section, 'killasgroup', stopasgroup))
exitcodes = list_of_exitcodes(get(section, 'exitcodes', '0'))
post_stop_command = get(section, 'post_stop_command', None)
post_stop_command_delay = integer(get(section, 'post_stop_command_delay', 0))
# see also redirect_stderr check in process_groups_from_parser()
redirect_stderr = boolean(get(section, 'redirect_stderr','false'))
numprocs = integer(get(section, 'numprocs', 1))
Expand Down Expand Up @@ -1019,6 +1021,11 @@ def get(section, opt, *args, **kwargs):
raise ValueError(
'program section %s does not specify a command' % section)

# Expand post_stop_command if provided
expanded_post_stop_command = post_stop_command
if expanded_post_stop_command:
expanded_post_stop_command = expand(expanded_post_stop_command, expansions, 'post_stop_command')

pconfig = klass(
self,
name=expand(process_name, expansions, 'process_name'),
Expand Down Expand Up @@ -1050,7 +1057,9 @@ def get(section, opt, *args, **kwargs):
exitcodes=exitcodes,
redirect_stderr=redirect_stderr,
environment=environment,
serverurl=serverurl)
serverurl=serverurl,
post_stop_command=expanded_post_stop_command,
post_stop_command_delay=post_stop_command_delay)

programs.append(pconfig)

Expand Down Expand Up @@ -1863,7 +1872,8 @@ class ProcessConfig(Config):
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stderr_events_enabled', 'stderr_syslog',
'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup',
'exitcodes', 'redirect_stderr' ]
'exitcodes', 'redirect_stderr', 'post_stop_command', 'post_stop_command_delay',
]
optional_param_names = [ 'environment', 'serverurl' ]

def __init__(self, options, **params):
Expand Down
107 changes: 106 additions & 1 deletion supervisor/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import signal
import shlex
import subprocess
import time
import traceback

Expand Down Expand Up @@ -30,6 +31,60 @@

from supervisor.socket_manager import SocketManager


# Python 2/3.6 compatibility: subprocess.run doesn't exist in Python 2, and
# capture_output parameter doesn't exist in Python 3.6
if True: # Always wrap to handle capture_output
if not hasattr(subprocess, 'run') and PY2:
import subprocess32 as subprocess
import collections

# Create a simple result object that mimics subprocess.CompletedProcess
_CompletedProcess = collections.namedtuple(
'CompletedProcess', ['returncode', 'stdout', 'stderr'],
)

_original_run = getattr(subprocess, 'run', None)

def _subprocess_run(cmd, shell=False, capture_output=False, timeout=None, text=False, **kwargs):
"""Compatibility wrapper for subprocess.run for Python 2 and Python 3.6"""
# Convert capture_output to explicit stdout/stderr pipes
if capture_output:
kwargs['stdout'] = subprocess.PIPE
kwargs['stderr'] = subprocess.PIPE

# If native subprocess.run exists, try to use it
if _original_run is not None:
try:
return _original_run(cmd, shell=shell, timeout=timeout, text=text, **kwargs)
except TypeError:
# capture_output not supported, fall through to Popen
pass

# Fall back to Popen for Python 2 or if run doesn't work
stdout_pipe = kwargs.get('stdout', None)
stderr_pipe = kwargs.get('stderr', None)
proc = None

try:
proc = subprocess.Popen(cmd, shell=shell, stdout=stdout_pipe, stderr=stderr_pipe)
stdout, stderr = proc.communicate(timeout=timeout)

if text:
stdout = stdout.decode('utf-8') if stdout else ''
stderr = stderr.decode('utf-8') if stderr else ''

return _CompletedProcess(returncode=proc.returncode, stdout=stdout, stderr=stderr)
except subprocess.TimeoutExpired:
if proc:
proc.kill()
proc.wait()
raise


subprocess.run = _subprocess_run


@functools.total_ordering
class Subprocess(object):

Expand Down Expand Up @@ -376,6 +431,51 @@ def _check_and_adjust_for_system_clock_rollback(self, test_time):
if self.delay > 0 and test_time < (self.delay - self.backoff):
self.delay = test_time + self.backoff

def _execute_post_stop_command(self, delay=0):
"""Execute the post_stop_command after the process has been stopped.

This method is called from finish() and will wait for the specified
delay before executing the command. This is a blocking call that
waits until the command completes before returning.
"""
if delay > 0:
time.sleep(delay)

processname = as_string(self.config.name)
logger = self.config.options.logger

if not self.config.post_stop_command:
return

logger.info('executing post_stop_command for process %s: %s' %
(processname, self.config.post_stop_command))

try:
# Execute the command using subprocess
result = subprocess.run(
self.config.post_stop_command,
shell=True,
capture_output=True,
timeout=30,
text=True
)

if result.returncode == 0:
logger.info('post_stop_command completed successfully for %s' % processname)
if result.stdout:
logger.debug('post_stop_command stdout: %s' % result.stdout.strip())
else:
logger.warn('post_stop_command failed for %s with exit code %s' %
(processname, result.returncode))
if result.stderr:
logger.warn('post_stop_command stderr: %s' % result.stderr.strip())
except subprocess.TimeoutExpired:
logger.error('post_stop_command timed out for process %s' % processname)
except Exception as e:
logger.error('error executing post_stop_command for %s: %s' %
(processname, str(e)))


def stop(self):
""" Administrative stop """
self.administrative_stop = True
Expand Down Expand Up @@ -573,7 +673,6 @@ def finish(self, pid, sts):
else:
self.config.options.logger.warn(msg)


elif too_quickly:
# the program did not stay up long enough to make it to RUNNING
# implies STARTING -> BACKOFF
Expand Down Expand Up @@ -612,6 +711,11 @@ def finish(self, pid, sts):
self.change_state(ProcessStates.EXITED, expected=False)
self.config.options.logger.warn(msg)

# Execute post_stop_command if configured, with delay
# This covers both EXITED states (expected and unexpected)
if self.config.post_stop_command:
self._execute_post_stop_command(delay=self.config.post_stop_command_delay)

self.pid = 0
self.config.options.close_parent_pipes(self.pipes)
self.pipes = {}
Expand Down Expand Up @@ -661,6 +765,7 @@ def transition(self):

logger = self.config.options.logger


if self.config.options.mood > SupervisorStates.RESTARTING:
# dont start any processes if supervisor is shutting down
if state == ProcessStates.EXITED:
Expand Down
2 changes: 2 additions & 0 deletions supervisor/rpcinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,8 @@ def getAllConfigInfo(self):
'stderr_logfile_maxbytes': pconfig.stderr_logfile_maxbytes,
'stderr_syslog': pconfig.stderr_syslog,
'serverurl': pconfig.serverurl,
'post_stop_command': pconfig.post_stop_command,
'post_stop_command_delay': pconfig.post_stop_command_delay,
}

# no support for these types in xml-rpc
Expand Down
4 changes: 4 additions & 0 deletions supervisor/skel/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false ; send stop signal to the UNIX process group (default false)
;killasgroup=false ; SIGKILL the UNIX process group (def false)
;post_stop_command= ; command to run after process is stopped (default none)
;post_stop_command_delay=0 ; delay in seconds before running post_stop_command (default 0)
;user=chrism ; setuid to this UNIX account to run the program
;redirect_stderr=true ; redirect proc stderr to stdout (default false)
;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO
Expand Down Expand Up @@ -138,6 +140,8 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false ; send stop signal to the UNIX process group (default false)
;killasgroup=false ; SIGKILL the UNIX process group (def false)
;post_stop_command= ; command to run after process is stopped (default none)
;post_stop_command_delay=0 ; delay in seconds before running post_stop_command (default 0)
;user=chrism ; setuid to this UNIX account to run the program
;redirect_stderr=false ; redirect_stderr=true is not allowed for eventlisteners
;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO
Expand Down
7 changes: 6 additions & 1 deletion supervisor/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def __init__(self):
self.umaskset = None
self.poller = DummyPoller(self)
self.silent = False
self.post_stop_command = None
self.post_stop_command_delay = None

def getLogger(self, *args, **kw):
logger = DummyLogger()
Expand Down Expand Up @@ -517,7 +519,8 @@ def __init__(self, options, name, command, directory=None, umask=None,
stderr_syslog=False,
redirect_stderr=False,
stopsignal=None, stopwaitsecs=10, stopasgroup=False, killasgroup=False,
exitcodes=(0,), environment=None, serverurl=None):
exitcodes=(0,), environment=None, serverurl=None,
post_stop_command_delay=None, post_stop_command=None):
self.options = options
self.name = name
self.command = command
Expand Down Expand Up @@ -553,6 +556,8 @@ def __init__(self, options, name, command, directory=None, umask=None,
self.umask = umask
self.autochildlogs_created = False
self.serverurl = serverurl
self.post_stop_command_delay = post_stop_command_delay
self.post_stop_command = post_stop_command

def get_path(self):
return ["/bin", "/usr/bin", "/usr/local/bin"]
Expand Down
9 changes: 6 additions & 3 deletions supervisor/tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3431,7 +3431,8 @@ def _makeOne(self, *arg, **kw):
'stderr_events_enabled', 'stderr_syslog',
'stopsignal', 'stopwaitsecs', 'stopasgroup',
'killasgroup', 'exitcodes', 'redirect_stderr',
'environment'):
'environment', 'post_stop_command', 'post_stop_command_delay',
):
defaults[name] = name
for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
Expand Down Expand Up @@ -3529,7 +3530,8 @@ def _makeOne(self, *arg, **kw):
'stderr_events_enabled', 'stderr_syslog',
'stopsignal', 'stopwaitsecs', 'stopasgroup',
'killasgroup', 'exitcodes', 'redirect_stderr',
'environment'):
'environment', 'post_stop_command', 'post_stop_command_delay',
):
defaults[name] = name
for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
Expand Down Expand Up @@ -3577,7 +3579,8 @@ def _makeOne(self, *arg, **kw):
'stderr_events_enabled', 'stderr_syslog',
'stopsignal', 'stopwaitsecs', 'stopasgroup',
'killasgroup', 'exitcodes', 'redirect_stderr',
'environment'):
'environment', 'post_stop_command', 'post_stop_command_delay',
):
defaults[name] = name
for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes',
'stderr_logfile_backups', 'stderr_logfile_maxbytes'):
Expand Down
Loading
Loading