source: mod_gnutls/test/mgstest/services.py @ e2200db

asyncioproxy-ticket
Last change on this file since e2200db was e2200db, checked in by Fiona Klute <fiona.klute@…>, 17 months ago

Test suite: Always run Apache with "-DFOREGROUND"

This simplifies process management a lot.

  • Property mode set to 100644
File size: 7.2 KB
Line 
1# Copyright 2019 Fiona Klute
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Handling services needed for mod_gnutls tests"""
16
17import errno
18import os
19import signal
20import subprocess
21import sys
22
23from contextlib import contextmanager
24from pathlib import Path
25from time import sleep
26
27class TestService:
28    """A generic service used in the mod_gnutls test environment."""
29
30    def __init__(self, start=None, stop=None, forking=False,
31                 env=None, condition=None, check=None, pidfile=None):
32        # command to start the service
33        self.start_command = start
34        # command to stop the service (otherwise use SIGTERM)
35        self.stop_command = stop
36        # forking: expect the start command to terminate during startup
37        self.forking = forking
38        # condition: start service if the function returns true
39        self.condition = condition or (lambda: True)
40
41        # child process for non-forking services
42        self.process = None
43        # Forking processes like apache2 require a PID file for
44        # tracking. The process must delete its PID file when exiting.
45        self.pidfile = Path(pidfile) if pidfile else None
46        if not self.pidfile and self.forking:
47            raise ValueError('Forking service must provide PID file!')
48        self.pid = None
49
50        # add environment variables for a subprocess only
51        if env:
52            self.process_env = os.environ.copy()
53            for name, value in env.items():
54                self.process_env[name] = value
55        else:
56            self.process_env = None
57
58        # check: function to check if the service is up and working
59        self.check = check
60
61        # sleep step for waiting (sec)
62        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
63
64    def start(self):
65        """Start the service"""
66        if not self.condition():
67            # skip
68            return
69        print(f'Starting: {self.start_command}')
70        if self.forking:
71            subprocess.run(self.start_command, check=True,
72                           env=self.process_env)
73        else:
74            self.process = subprocess.Popen(self.start_command,
75                                            env=self.process_env,
76                                            close_fds=True)
77
78    def stop(self):
79        """Order the service to stop"""
80        if not self.condition():
81            # skip
82            return
83        # Read PID file before actually sending the stop signal
84        if self.pidfile:
85            if not self.pidfile.exists():
86                print(f'Skipping stop of {self.stop_command}, no PID file!')
87                # Presumably the process isn't running, ignore.
88                return
89            self.pid = int(self.pidfile.read_text())
90        if self.stop_command:
91            print(f'Stopping: {self.stop_command}')
92            subprocess.run(self.stop_command, check=True, env=self.process_env)
93        else:
94            print(f'Stopping (SIGTERM): {self.start_command}')
95            if self.process:
96                # non-forking process: direct SIGTERM to child process
97                self.process.terminate()
98            else:
99                # forking process: SIGTERM based on PID file
100                os.kill(self.pid, signal.SIGTERM)
101
102    def wait(self):
103        """Wait for the process to actually stop after calling stop().
104
105        WARNING: Calling this method without stop() first is likely to
106        hang.
107        """
108        if self.process:
109            self.process.wait()
110            self.process = None
111        elif self.pid and self.pidfile:
112            print(f'Waiting for PID {self.pid} to delete its PID file '
113                  f'({self.pidfile})...', file=sys.stderr)
114            sys.stderr.flush()
115            while True:
116                if self.pidfile.exists():
117                    sleep(self._step)
118                else:
119                    break
120            self.pid = None
121
122    def wait_ready(self, timeout=None):
123        """Wait for the started service to be ready.
124
125        The function passed to the constructor as "check" is called to
126        determine whether it is. Waiting also ends if self.process
127        terminates.
128
129        Returns: None if the service is ready, or the return code if
130        the process has terminated.
131
132        Raises a TimeoutError if the given timeout has been exceeded.
133
134        """
135        if not self.check:
136            return None
137
138        slept = 0
139        while not timeout or slept < timeout:
140            if self.process and self.process.poll():
141                return self.process.returncode
142            if self.check():
143                return None
144            else:
145                sleep(self._step)
146                slept = slept + self._step
147        # TODO: A custom ServiceException or something would be nicer
148        # here.
149        raise TimeoutError('Waiting for service timed out!')
150
151    @contextmanager
152    def run(self):
153        """Context manager to start and stop a service. Note that entering the
154        context does not call TestService.wait_ready() on the service,
155        you must do that separately if desired.
156
157        """
158        try:
159            self.start()
160            # TODO: with async execution we could also call
161            # wait_ready() here
162            yield self
163        finally:
164            self.stop()
165            # TODO: this would really benefit from async execution
166            self.wait()
167
168
169
170class ApacheService(TestService):
171    """An Apache HTTPD instance used in the mod_gnutls test
172    environment."""
173
174    apache2 = os.environ.get('APACHE2', 'apache2')
175
176    def __init__(self, config, env=None, pidfile=None, check=None,
177                 valgrind_log=None):
178        self.config = Path(config).resolve()
179        base_cmd = [self.apache2, '-f', str(self.config), '-k']
180        start_cmd = base_cmd + ['start', '-DFOREGROUND']
181        if valgrind_log:
182            start_cmd = ['valgrind', '-s', '--leak-check=full',
183                         '--track-origins=yes', '--vgdb=no',
184                         f'--log-file={valgrind_log}'] \
185                         + start_cmd
186        if not check:
187            check = self.pidfile_check
188        super(ApacheService, self).__init__(start=start_cmd,
189                                            stop=base_cmd + ['stop'],
190                                            forking=False,
191                                            env=env,
192                                            pidfile=pidfile,
193                                            condition=self.config_exists,
194                                            check=check)
195
196    def config_exists(self):
197        return self.config.is_file()
198
199    def pidfile_check(self):
200        """Default check method for ApacheService, waits for the PID file to
201        be present."""
202        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.