source: mod_gnutls/test/mgstest/services.py @ 05984a0

proxy-ticket
Last change on this file since 05984a0 was 05984a0, checked in by Fiona Klute <fiona.klute@…>, 6 months ago

Replace "runtests" with "runtest.py"

This is the next step from handling HTTP requests and responses in
Python. In particular error handling is a lot easier to do in Python
than using Bash trap functions.

  • Property mode set to 100644
File size: 6.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 os.path
20import signal
21import subprocess
22import sys
23
24from contextlib import contextmanager
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        if condition:
40            self.condition = condition
41        else:
42            self.condition = lambda: True
43
44        # child process for non-forking services
45        self.process = None
46        # Forking processes like apache2 require a PID file for
47        # tracking. The process must delete its PID file when exiting.
48        self.pidfile = pidfile
49        if not self.pidfile and self.forking:
50            raise ValueError('Forking service must provide PID file!')
51        self.pid = None
52
53        # add environment variables for a subprocess only
54        if env:
55            self.process_env = os.environ.copy()
56            for name, value in env.items():
57                self.process_env[name] = value
58        else:
59            self.process_env = None
60
61        # check: function to check if the service is up and working
62        self.check = check
63
64        # sleep step for waiting (sec)
65        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
66
67    def start(self):
68        """Start the service"""
69        if not self.condition():
70            # skip
71            return
72        print(f'Starting: {self.start_command}')
73        if self.forking:
74            subprocess.run(self.start_command, check=True,
75                           env=self.process_env)
76        else:
77            self.process = subprocess.Popen(self.start_command,
78                                            env=self.process_env,
79                                            close_fds=True)
80
81    def stop(self):
82        """Order the service to stop"""
83        if not self.condition():
84            # skip
85            return
86        # Read PID file before actually sending the stop signal
87        if self.pidfile:
88            if not os.path.exists(self.pidfile):
89                print(f'Skipping stop of {self.stop_command}, no PID file!')
90                # Presumably the process isn't running, ignore.
91                return
92            with open(self.pidfile, 'r') as file:
93                self.pid = int(file.read())
94        if self.stop_command:
95            print(f'Stopping: {self.stop_command}')
96            subprocess.run(self.stop_command, check=True, env=self.process_env)
97        else:
98            print(f'Stopping (SIGTERM): {self.start_command}')
99            if self.process:
100                # non-forking process: direct SIGTERM to child process
101                self.process.terminate()
102            else:
103                # forking process: SIGTERM based on PID file
104                os.kill(self.pid, signal.SIGTERM)
105
106    def wait(self):
107        """Wait for the process to actually stop after calling stop().
108
109        WARNING: Calling this method without stop() first is likely to
110        hang.
111        """
112        if self.process:
113            self.process.wait()
114            self.process = None
115        elif self.pid:
116            print(f'Waiting for PID {self.pid}...', file=sys.stderr)
117            while True:
118                try:
119                    # signal 0 just checks if the target process exists
120                    os.kill(self.pid, 0)
121                    sleep(self._step)
122                except OSError as e:
123                    if e.errno != errno.ESRCH:
124                        print('Unexpected exception while checking process '
125                              f'state: {e}', file=sys.stderr)
126                    break
127            self.pid = None
128
129    def wait_ready(self, timeout=None):
130        """Wait for the started service to be ready. The function passed to
131        the constructor as "check" is called to determine whether it is."""
132        if not self.check:
133            return
134
135        slept = 0
136        while not timeout or slept < timeout:
137            if self.check():
138                return
139            else:
140                sleep(self._step)
141                slept = slept + self._step
142        # TODO: A custom ServiceException or something would be nicer
143        # here.
144        raise TimeoutError('Waiting for service timed out!')
145
146    @contextmanager
147    def run(self):
148        """Context manager to start and stop a service. Note that entering the
149        context does not call TestService.wait_ready() on the service,
150        you must do that separately if desired.
151
152        """
153        try:
154            self.start()
155            # TODO: with async execution we could also call
156            # wait_ready() here
157            yield self
158        finally:
159            self.stop()
160            # TODO: this would really benefit from async execution
161            self.wait()
162
163
164
165class ApacheService(TestService):
166    """An Apache HTTPD instance used in the mod_gnutls test
167    environment."""
168
169    apache2 = os.environ.get('APACHE2', 'apache2')
170
171    def __init__(self, config, env=None, pidfile=None, check=None):
172        self.config = os.path.realpath(config)
173        base_cmd = [self.apache2, '-f', str(self.config), '-k']
174        if not check:
175            check = self.pidfile_check
176        super(ApacheService, self).__init__(start=base_cmd + ['start'],
177                                            stop=base_cmd + ['stop'],
178                                            forking=True,
179                                            env=env,
180                                            pidfile=pidfile,
181                                            condition=self.config_exists,
182                                            check=check)
183
184    def config_exists(self):
185        return os.path.isfile(self.config)
186
187    def pidfile_check(self):
188        """Default check method for ApacheService, waits for the PID file to
189        be present."""
190        return os.path.exists(self.pidfile)
Note: See TracBrowser for help on using the repository browser.