diff --git a/supervisor/options.py b/supervisor/options.py index 7b53cc760..3565243bf 100644 --- a/supervisor/options.py +++ b/supervisor/options.py @@ -932,6 +932,19 @@ def get(section, opt, *args, **kwargs): serverurl = get(section, 'serverurl', None) if serverurl and serverurl.strip().upper() == 'AUTO': serverurl = None + runningregex = get(section, 'runningregex', None) + if runningregex: + try: + runningregex = re.compile(r'.*'+ runningregex) + except Exception as e: + raise ValueError( + f"program section {section} has invalid runningregex value. Error {e}") + if "/dev/" in get(section, 'stdout_logfile'): + self.warnings.warn( + '\033[93m runningregex is only supported for logfiles.' + 'It does not work with /dev/null or /dev/fd/1.' + 'Startsecs are used instead \033[0m') + runningregex = None # find uid from "user" option user = get(section, 'user', None) @@ -1057,7 +1070,9 @@ def get(section, opt, *args, **kwargs): exitcodes=exitcodes, redirect_stderr=redirect_stderr, environment=environment, - serverurl=serverurl) + serverurl=serverurl, + runningregex=runningregex + ) programs.append(pconfig) @@ -1875,7 +1890,7 @@ class ProcessConfig(Config): 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', 'redirect_stderr' ] - optional_param_names = [ 'environment', 'serverurl' ] + optional_param_names = [ 'environment', 'serverurl', 'runningregex' ] def __init__(self, options, **params): self.options = options diff --git a/supervisor/process.py b/supervisor/process.py index d6f60f3e2..96181d356 100644 --- a/supervisor/process.py +++ b/supervisor/process.py @@ -5,6 +5,7 @@ import shlex import time import traceback +import re from supervisor.compat import maxint from supervisor.compat import as_bytes @@ -21,6 +22,7 @@ from supervisor.options import decode_wait_status from supervisor.options import signame from supervisor.options import ProcessException, BadCommand +from supervisor.options import readFile from supervisor.dispatchers import EventListenerStates @@ -194,6 +196,17 @@ def spawn(self): Return the process id. If the fork() call fails, return None. """ + # if runningregex is used, set the log_offset correctly + if self.config.runningregex is not None: + try: + logfile = getattr(self.config, 'stdout_logfile') + with open(logfile, 'rb') as f: + log_file_length = len(f.read()) + self.log_offset = log_file_length + except FileNotFoundError: + self.log_offset = 0 + + options = self.config.options processname = as_string(self.config.name) @@ -679,7 +692,7 @@ def transition(self): processname = as_string(self.config.name) if state == ProcessStates.STARTING: - if now - self.laststart > self.config.startsecs: + if now - self.laststart > self.config.startsecs and not self.config.runningregex: # STARTING -> RUNNING if the proc has started # successfully and it has stayed up for at least # proc.config.startsecs, @@ -692,6 +705,33 @@ def transition(self): '> than %s seconds (startsecs)' % self.config.startsecs) logger.info('success: %s %s' % (processname, msg)) + if self.config.runningregex: + logfile = getattr(self.config, 'stdout_logfile') + try: + logfile_as_str = as_string(readFile(logfile, self.log_offset, 0)) + + # delete ascii escape sequence and newlines with regular expression + ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]|\n') + logfile_as_str = ansi_escape.sub('', logfile_as_str) + + # STARTING -> RUNNING if the process has started + # successfully and the runningregex is met + if self.config.runningregex.match(logfile_as_str): + self.delay = 0 + self.backoff = 0 + self._assertInState(ProcessStates.STARTING) + self.change_state(ProcessStates.RUNNING) + msg = ('entered RUNNING state, found runningregex in stdout') + logger.info('success: %s %s' % (processname, msg)) + except ValueError: + with open(logfile, 'w') as file: + file.write("") + logger.warn( + 'logfile {} did not exists. Created new logfile, ' + 'runnngregex match will be checked again next ' + 'iteration.'.format(logfile)) + + if state == ProcessStates.BACKOFF: if self.backoff > self.config.startretries: # BACKOFF -> FATAL if the proc has exceeded its number