source: mod_gnutls/test/mgstest/services.py @ 264ab17

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

mgstest.services: Remove code for handling forking services

No longer needed since running Apache with "-DFOREGROUND", and
removing it simplifies process management a lot.

  • Property mode set to 100644
File size: 5.8 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, env=None,
31                 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        # condition: start service if the function returns true
37        self.condition = condition or (lambda: True)
38
39        # child process
40        self.process = None
41        # PID file, if any. The process must delete its PID file when
42        # exiting.
43        self.pidfile = Path(pidfile) if pidfile else None
44
45        # add environment variables for a subprocess only
46        if env:
47            self.process_env = os.environ.copy()
48            for name, value in env.items():
49                self.process_env[name] = value
50        else:
51            self.process_env = None
52
53        # check: function to check if the service is up and working
54        self.check = check
55
56        # sleep step for waiting (sec)
57        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
58
59    def start(self):
60        """Start the service"""
61        if not self.condition():
62            # skip
63            return
64        print(f'Starting: {self.start_command}')
65        self.process = subprocess.Popen(self.start_command,
66                                        env=self.process_env,
67                                        close_fds=True)
68
69    def stop(self):
70        """Order the service to stop"""
71        if not self.condition():
72            # skip
73            return
74        if not self.process or self.process.poll():
75            # process either never started or already stopped
76            return
77
78        if self.stop_command:
79            print(f'Stopping: {self.stop_command}')
80            subprocess.run(self.stop_command, check=True, env=self.process_env)
81        else:
82            print(f'Stopping (SIGTERM): {self.start_command}')
83            self.process.terminate()
84
85    def wait(self):
86        """Wait for the process to actually stop after calling stop().
87
88        WARNING: Calling this method without stop() first is likely to
89        hang.
90        """
91        if self.process:
92            self.process.wait()
93            self.process = None
94
95    def wait_ready(self, timeout=None):
96        """Wait for the started service to be ready.
97
98        The function passed to the constructor as "check" is called to
99        determine whether it is. Waiting also ends if self.process
100        terminates.
101
102        Returns: None if the service is ready, or the return code if
103        the process has terminated.
104
105        Raises a TimeoutError if the given timeout has been exceeded.
106
107        """
108        if not self.check:
109            return None
110
111        slept = 0
112        while not timeout or slept < timeout:
113            if self.process and self.process.poll():
114                return self.process.returncode
115            if self.check():
116                return None
117            else:
118                sleep(self._step)
119                slept = slept + self._step
120        # TODO: A custom ServiceException or something would be nicer
121        # here.
122        raise TimeoutError('Waiting for service timed out!')
123
124    @contextmanager
125    def run(self):
126        """Context manager to start and stop a service. Note that entering the
127        context does not call TestService.wait_ready() on the service,
128        you must do that separately if desired.
129
130        """
131        try:
132            self.start()
133            # TODO: with async execution we could also call
134            # wait_ready() here
135            yield self
136        finally:
137            self.stop()
138            # TODO: this would really benefit from async execution
139            self.wait()
140
141
142
143class ApacheService(TestService):
144    """An Apache HTTPD instance used in the mod_gnutls test
145    environment."""
146
147    apache2 = os.environ.get('APACHE2', 'apache2')
148
149    def __init__(self, config, env=None, pidfile=None, check=None,
150                 valgrind_log=None):
151        self.config = Path(config).resolve()
152        base_cmd = [self.apache2, '-f', str(self.config), '-k']
153        start_cmd = base_cmd + ['start', '-DFOREGROUND']
154        if valgrind_log:
155            start_cmd = ['valgrind', '-s', '--leak-check=full',
156                         '--track-origins=yes', '--vgdb=no',
157                         f'--log-file={valgrind_log}'] \
158                         + start_cmd
159        if not check:
160            check = self.pidfile_check
161        super(ApacheService, self).__init__(start=start_cmd,
162                                            stop=base_cmd + ['stop'],
163                                            env=env,
164                                            pidfile=pidfile,
165                                            condition=self.config_exists,
166                                            check=check)
167
168    def config_exists(self):
169        return self.config.is_file()
170
171    def pidfile_check(self):
172        """Default check method for ApacheService, waits for the PID file to
173        be present."""
174        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.