source: mod_gnutls/test/mgstest/services.py @ 573b810

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

mgstest.services: Use pathlib and conditional expressions

Just a minor simplification.

  • Property mode set to 100644
File size: 6.7 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:
112            print(f'Waiting for PID {self.pid}...', file=sys.stderr)
113            while True:
114                try:
115                    # signal 0 just checks if the target process exists
116                    os.kill(self.pid, 0)
117                    sleep(self._step)
118                except OSError as e:
119                    if e.errno != errno.ESRCH:
120                        print('Unexpected exception while checking process '
121                              f'state: {e}', file=sys.stderr)
122                    break
123            self.pid = None
124
125    def wait_ready(self, timeout=None):
126        """Wait for the started service to be ready. The function passed to
127        the constructor as "check" is called to determine whether it is."""
128        if not self.check:
129            return
130
131        slept = 0
132        while not timeout or slept < timeout:
133            if self.check():
134                return
135            else:
136                sleep(self._step)
137                slept = slept + self._step
138        # TODO: A custom ServiceException or something would be nicer
139        # here.
140        raise TimeoutError('Waiting for service timed out!')
141
142    @contextmanager
143    def run(self):
144        """Context manager to start and stop a service. Note that entering the
145        context does not call TestService.wait_ready() on the service,
146        you must do that separately if desired.
147
148        """
149        try:
150            self.start()
151            # TODO: with async execution we could also call
152            # wait_ready() here
153            yield self
154        finally:
155            self.stop()
156            # TODO: this would really benefit from async execution
157            self.wait()
158
159
160
161class ApacheService(TestService):
162    """An Apache HTTPD instance used in the mod_gnutls test
163    environment."""
164
165    apache2 = os.environ.get('APACHE2', 'apache2')
166
167    def __init__(self, config, env=None, pidfile=None, check=None):
168        self.config = Path(config).resolve()
169        base_cmd = [self.apache2, '-f', str(self.config), '-k']
170        if not check:
171            check = self.pidfile_check
172        super(ApacheService, self).__init__(start=base_cmd + ['start'],
173                                            stop=base_cmd + ['stop'],
174                                            forking=True,
175                                            env=env,
176                                            pidfile=pidfile,
177                                            condition=self.config_exists,
178                                            check=check)
179
180    def config_exists(self):
181        return self.config.is_file()
182
183    def pidfile_check(self):
184        """Default check method for ApacheService, waits for the PID file to
185        be present."""
186        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.