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