• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#***************************************************************************
4#                                  _   _ ____  _
5#  Project                     ___| | | |  _ \| |
6#                             / __| | | | |_) | |
7#                            | (__| |_| |  _ <| |___
8#                             \___|\___/|_| \_\_____|
9#
10# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
11#
12# This software is licensed as described in the file COPYING, which
13# you should have received as part of this distribution. The terms
14# are also available at https://curl.se/docs/copyright.html.
15#
16# You may opt to use, copy, modify, merge, publish, distribute and/or sell
17# copies of the Software, and permit persons to whom the Software is
18# furnished to do so, under the terms of the COPYING file.
19#
20# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
21# KIND, either express or implied.
22#
23# SPDX-License-Identifier: curl
24#
25###########################################################################
26#
27import logging
28import os
29import signal
30import subprocess
31import time
32from typing import Optional
33from datetime import datetime, timedelta
34
35from .env import Env
36from .curl import CurlClient
37
38
39log = logging.getLogger(__name__)
40
41
42class Nghttpx:
43
44    def __init__(self, env: Env):
45        self.env = env
46        self._cmd = env.nghttpx
47        self._run_dir = os.path.join(env.gen_dir, 'nghttpx')
48        self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid')
49        self._conf_file = os.path.join(self._run_dir, 'nghttpx.conf')
50        self._error_log = os.path.join(self._run_dir, 'nghttpx.log')
51        self._stderr = os.path.join(self._run_dir, 'nghttpx.stderr')
52        self._tmp_dir = os.path.join(self._run_dir, 'tmp')
53        self._process = None
54        self._process: Optional[subprocess.Popen] = None
55        self._rmf(self._pid_file)
56        self._rmf(self._error_log)
57        self._mkpath(self._run_dir)
58        self._write_config()
59
60    def exists(self):
61        return os.path.exists(self._cmd)
62
63    def clear_logs(self):
64        self._rmf(self._error_log)
65        self._rmf(self._stderr)
66
67    def is_running(self):
68        if self._process:
69            self._process.poll()
70            return self._process.returncode is None
71        return False
72
73    def start_if_needed(self):
74        if not self.is_running():
75            return self.start()
76        return True
77
78    def start(self, wait_live=True):
79        self._mkpath(self._tmp_dir)
80        if self._process:
81            self.stop()
82        args = [
83            self._cmd,
84            f'--frontend=*,{self.env.h3_port};quic',
85            f'--backend=127.0.0.1,{self.env.https_port};{self.env.domain1};sni={self.env.domain1};proto=h2;tls',
86            f'--backend=127.0.0.1,{self.env.http_port}',
87            f'--log-level=INFO',
88            f'--pid-file={self._pid_file}',
89            f'--errorlog-file={self._error_log}',
90            f'--conf={self._conf_file}',
91            f'--cacert={self.env.ca.cert_file}',
92            self.env.get_credentials(self.env.domain1).pkey_file,
93            self.env.get_credentials(self.env.domain1).cert_file,
94        ]
95        ngerr = open(self._stderr, 'a')
96        self._process = subprocess.Popen(args=args, stderr=ngerr)
97        if self._process.returncode is not None:
98            return False
99        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
100
101    def stop_if_running(self):
102        if self.is_running():
103            return self.stop()
104        return True
105
106    def stop(self, wait_dead=True):
107        self._mkpath(self._tmp_dir)
108        if self._process:
109            self._process.terminate()
110            self._process.wait(timeout=2)
111            self._process = None
112            return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
113        return True
114
115    def restart(self):
116        self.stop()
117        return self.start()
118
119    def reload(self, timeout: timedelta):
120        if self._process:
121            running = self._process
122            self._process = None
123            os.kill(running.pid, signal.SIGQUIT)
124            end_wait = datetime.now() + timeout
125            if not self.start(wait_live=False):
126                self._process = running
127                return False
128            while datetime.now() < end_wait:
129                try:
130                    log.debug(f'waiting for nghttpx({running.pid}) to exit.')
131                    running.wait(2)
132                    log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
133                    break
134                except subprocess.TimeoutExpired:
135                    log.warning(f'nghttpx({running.pid}), not shut down yet.')
136                    os.kill(running.pid, signal.SIGQUIT)
137            if datetime.now() >= end_wait:
138                log.error(f'nghttpx({running.pid}), terminate forcefully.')
139                os.kill(running.pid, signal.SIGKILL)
140                running.terminate()
141                running.wait(1)
142            return self.wait_live(timeout=timedelta(seconds=5))
143        return False
144
145    def wait_dead(self, timeout: timedelta):
146        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
147        try_until = datetime.now() + timeout
148        while datetime.now() < try_until:
149            check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
150            r = curl.http_get(url=check_url, extra_args=['--http3-only'])
151            if r.exit_code != 0:
152                return True
153            log.debug(f'waiting for nghttpx to stop responding: {r}')
154            time.sleep(.1)
155        log.debug(f"Server still responding after {timeout}")
156        return False
157
158    def wait_live(self, timeout: timedelta):
159        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
160        try_until = datetime.now() + timeout
161        while datetime.now() < try_until:
162            check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
163            r = curl.http_get(url=check_url, extra_args=[
164                '--http3-only', '--trace', 'curl.trace', '--trace-time'
165            ])
166            if r.exit_code == 0:
167                return True
168            log.debug(f'waiting for nghttpx to become responsive: {r}')
169            time.sleep(.1)
170        log.error(f"Server still not responding after {timeout}")
171        return False
172
173    def _rmf(self, path):
174        if os.path.exists(path):
175            return os.remove(path)
176
177    def _mkpath(self, path):
178        if not os.path.exists(path):
179            return os.makedirs(path)
180
181    def _write_config(self):
182        with open(self._conf_file, 'w') as fd:
183            fd.write(f'# nghttpx test config'),
184            fd.write("\n".join([
185                '# do we need something here?'
186            ]))
187