connections: introduce http/3 happy eyeballs

New cfilter HTTP-CONNECT for h3/h2/http1.1 eyeballing.
- filter is installed when `--http3` in the tool is used (or
  the equivalent CURLOPT_ done in the library)
- starts a QUIC/HTTP/3 connect right away. Should that not
  succeed after 100ms (subject to change), a parallel attempt
  is started for HTTP/2 and HTTP/1.1 via TCP
- both attempts are subject to IPv6/IPv4 eyeballing, same
  as happens for other connections
- tie timeout to the ip-version HAPPY_EYEBALLS_TIMEOUT
- use a `soft` timeout at half the value. When the soft timeout
  expires, the HTTPS-CONNECT filter checks if the QUIC filter
  has received any data from the server. If not, it will start
  the HTTP/2 attempt.

HTTP/3(ngtcp2) improvements.
- setting call_data in all cfilter calls similar to http/2 and vtls filters
  for use in callback where no stream data is available.
- returning CURLE_PARTIAL_FILE for prematurely terminated transfers
- enabling pytest test_05 for h3
- shifting functionality to "connect" UDP sockets from ngtcp2
  implementation into the udp socket cfilter. Because unconnected
  UDP sockets are weird. For example they error when adding to a
  pollset.

HTTP/3(quiche) improvements.
- fixed upload bug in quiche implementation, now passes 251 and pytest
- error codes on stream RESET
- improved debug logs
- handling of DRAIN during connect
- limiting pending event queue

HTTP/2 cfilter improvements.
- use LOG_CF macros for dynamic logging in debug build
- fix CURLcode on RST streams to be CURLE_PARTIAL_FILE
- enable pytest test_05 for h2
- fix upload pytests and improve parallel transfer performance.

GOAWAY handling for ngtcp2/quiche
- during connect, when the remote server refuses to accept new connections
  and closes immediately (so the local conn goes into DRAIN phase), the
  connection is torn down and a another attempt is made after a short grace
  period.
  This is the behaviour observed with nghttpx when we tell it to  shut
  down gracefully. Tested in pytest test_03_02.

TLS improvements
- ALPN selection for SSL/SSL-PROXY filters in one vtls set of functions, replaces
  copy of logic in all tls backends.
- standardized the infof logging of offered ALPNs
- ALPN negotiated: have common function for all backends that sets alpn proprty
  and connection related things based on the negotiated protocol (or lack thereof).

- new tests/tests-httpd/scorecard.py for testing h3/h2 protocol implementation.
  Invoke:
    python3 tests/tests-httpd/scorecard.py --help
  for usage.

Improvements on gathering connect statistics and socket access.
- new CF_CTRL_CONN_REPORT_STATS cfilter control for having cfilters
  report connection statistics. This is triggered when the connection
  has completely connected.
- new void Curl_pgrsTimeWas(..) method to report a timer update with
  a timestamp of when it happend. This allows for updating timers
  "later", e.g. a connect statistic after full connectivity has been
  reached.
- in case of HTTP eyeballing, the previous changes will update
  statistics only from the filter chain that "won" the eyeballing.
- new cfilter query CF_QUERY_SOCKET for retrieving the socket used
  by a filter chain.
  Added methods Curl_conn_cf_get_socket() and Curl_conn_get_socket()
  for convenient use of this query.
- Change VTLS backend to query their sub-filters for the socket when
  checks during the handshake are made.

HTTP/3 documentation on how https eyeballing works.

TLS improvements
- ALPN selection for SSL/SSL-PROXY filters in one vtls set of functions, replaces
  copy of logic in all tls backends.
- standardized the infof logging of offered ALPNs
- ALPN negotiated: have common function for all backends that sets alpn proprty
  and connection related things based on the negotiated protocol (or lack thereof).

Scorecard with Caddy.
- configure can be run with `--with-test-caddy=path` to specify which caddy to use for testing
- tests/tests-httpd/scorecard.py now measures download speeds with caddy

pytest improvements
- adding Makfile to clean gen dir
- adding nghttpx rundir creation on start
- checking httpd version 2.4.55 for test_05 cases where it is needed. Skipping with message if too old.
- catch exception when checking for caddy existance on system.

Closes #10349
This commit is contained in:
Stefan Eissing 2023-02-01 17:13:12 +01:00 committed by Daniel Stenberg
parent b7aaf074e5
commit 671158242d
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
61 changed files with 3599 additions and 1249 deletions

View file

@ -26,6 +26,7 @@
#
from .env import Env
from .certs import TestCA, Credentials
from .caddy import Caddy
from .httpd import Httpd
from .curl import CurlClient, ExecResult
from .nghttpx import Nghttpx

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) 2008 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
#
import logging
import os
import subprocess
import time
from datetime import timedelta, datetime
from json import JSONEncoder
from .curl import CurlClient
from .env import Env
log = logging.getLogger(__name__)
class Caddy:
def __init__(self, env: Env):
self.env = env
self._caddy = os.environ['CADDY'] if 'CADDY' in os.environ else env.caddy
self._caddy_dir = os.path.join(env.gen_dir, 'caddy')
self._docs_dir = os.path.join(self._caddy_dir, 'docs')
self._conf_file = os.path.join(self._caddy_dir, 'Caddyfile')
self._error_log = os.path.join(self._caddy_dir, 'caddy.log')
self._tmp_dir = os.path.join(self._caddy_dir, 'tmp')
self._process = None
self._rmf(self._error_log)
@property
def docs_dir(self):
return self._docs_dir
def clear_logs(self):
self._rmf(self._error_log)
def is_running(self):
if self._process:
self._process.poll()
return self._process.returncode is None
return False
def start_if_needed(self):
if not self.is_running():
return self.start()
return True
def start(self, wait_live=True):
self._mkpath(self._tmp_dir)
if self._process:
self.stop()
self._write_config()
args = [
self._caddy, 'run'
]
caddyerr = open(self._error_log, 'a')
self._process = subprocess.Popen(args=args, cwd=self._caddy_dir, stderr=caddyerr)
if self._process.returncode is not None:
return False
return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
def stop_if_running(self):
if self.is_running():
return self.stop()
return True
def stop(self, wait_dead=True):
self._mkpath(self._tmp_dir)
if self._process:
self._process.terminate()
self._process.wait(timeout=2)
self._process = None
return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
return True
def restart(self):
self.stop()
return self.start()
def wait_dead(self, timeout: timedelta):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
while datetime.now() < try_until:
check_url = f'https://{self.env.domain1}:{self.env.caddy_port}/'
r = curl.http_get(url=check_url)
if r.exit_code != 0:
return True
log.debug(f'waiting for caddy to stop responding: {r}')
time.sleep(.1)
log.debug(f"Server still responding after {timeout}")
return False
def wait_live(self, timeout: timedelta):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
while datetime.now() < try_until:
check_url = f'https://{self.env.domain1}:{self.env.caddy_port}/'
r = curl.http_get(url=check_url)
if r.exit_code == 0:
return True
log.error(f'curl: {r}')
log.debug(f'waiting for caddy to become responsive: {r}')
time.sleep(.1)
log.error(f"Server still not responding after {timeout}")
return False
def _rmf(self, path):
if os.path.exists(path):
return os.remove(path)
def _mkpath(self, path):
if not os.path.exists(path):
return os.makedirs(path)
def _write_config(self):
domain1 = self.env.domain1
creds1 = self.env.get_credentials(domain1)
self._mkpath(self._docs_dir)
self._mkpath(self._tmp_dir)
with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd:
data = {
'server': f'{domain1}',
}
fd.write(JSONEncoder().encode(data))
with open(self._conf_file, 'w') as fd:
conf = [ # base server config
f'{{',
f' https_port {self.env.caddy_port}',
f' servers :{self.env.caddy_port} {{',
f' protocols h3 h2 h1',
f' }}',
f'}}',
f'{domain1}:{self.env.caddy_port} {{',
f' file_server * {{',
f' root {self._docs_dir}',
f' }}',
f' tls {creds1.cert_file} {creds1.pkey_file}',
f'}}',
]
fd.write("\n".join(conf))

View file

@ -155,28 +155,36 @@ class ExecResult:
def add_assets(self, assets: List):
self._assets.extend(assets)
def check_responses(self, count: int, exp_status: Optional[int] = None):
if len(self.responses) != count:
seen_queries = []
for idx, resp in enumerate(self.responses):
assert resp['status'] == 200, f'response #{idx} status: {resp["status"]}'
if 'rquery' not in resp['header']:
log.error(f'response #{idx} missing "rquery": {resp["header"]}')
seen_queries.append(int(resp['header']['rquery']))
for i in range(0, count-1):
if i not in seen_queries:
log.error(f'response for query {i} missing')
if self.with_stats and len(self.stats) == count:
log.error(f'got all {count} stats, though')
def check_responses(self, count: int, exp_status: Optional[int] = None,
exp_exitcode: Optional[int] = None):
assert len(self.responses) == count, \
f'response count: expected {count}, got {len(self.responses)}'
if exp_status is not None:
for idx, x in enumerate(self.responses):
assert x['status'] == exp_status, \
f'response #{idx} unexpectedstatus: {x["status"]}'
if exp_exitcode is not None:
for idx, x in enumerate(self.responses):
if 'exitcode' in x:
assert x['exitcode'] == 0, f'response #{idx} exitcode: {x["exitcode"]}'
if self.with_stats:
assert len(self.stats) == count, f'{self}'
def check_stats(self, count: int, exp_status: Optional[int] = None,
exp_exitcode: Optional[int] = None):
assert len(self.stats) == count, \
f'stats count: expected {count}, got {len(self.stats)}'
if exp_status is not None:
for idx, x in enumerate(self.stats):
assert 'http_code' in x, \
f'status #{idx} reports no http_code'
assert x['http_code'] == exp_status, \
f'status #{idx} unexpected http_code: {x["http_code"]}'
if exp_exitcode is not None:
for idx, x in enumerate(self.stats):
if 'exitcode' in x:
assert x['exitcode'] == 0, f'status #{idx} exitcode: {x["exitcode"]}'
class CurlClient:
@ -186,7 +194,7 @@ class CurlClient:
'http/1.1': '--http1.1',
'h2': '--http2',
'h2c': '--http2',
'h3': '--http3',
'h3': '--http3-only',
}
def __init__(self, env: Env, run_dir: Optional[str] = None):
@ -219,6 +227,7 @@ class CurlClient:
def http_download(self, urls: List[str],
alpn_proto: Optional[str] = None,
with_stats: bool = True,
with_headers: bool = False,
extra_args: List[str] = None):
if extra_args is None:
extra_args = []
@ -230,7 +239,41 @@ class CurlClient:
'-w', '%{json}\\n'
])
return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
with_stats=with_stats)
with_stats=with_stats,
with_headers=with_headers)
def http_upload(self, urls: List[str], data: str,
alpn_proto: Optional[str] = None,
with_stats: bool = True,
with_headers: bool = False,
extra_args: Optional[List[str]] = None):
if extra_args is None:
extra_args = []
extra_args.extend([
'--data-binary', data, '-o', 'download_#1.data',
])
if with_stats:
extra_args.extend([
'-w', '%{json}\\n'
])
return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
with_stats=with_stats,
with_headers=with_headers)
def response_file(self, idx: int):
return os.path.join(self._run_dir, f'download_{idx}.data')
def run_direct(self, args, with_stats: bool = False):
my_args = [self._curl]
if with_stats:
my_args.extend([
'-w', '%{json}\\n'
])
my_args.extend([
'-o', 'download.data',
])
my_args.extend(args)
return self._run(args=my_args, with_stats=with_stats)
def _run(self, args, intext='', with_stats: bool = False):
self._rmf(self._stdoutfile)
@ -252,12 +295,15 @@ class CurlClient:
def _raw(self, urls, timeout=10, options=None, insecure=False,
alpn_proto: Optional[str] = None,
force_resolve=True, with_stats=False):
force_resolve=True,
with_stats=False,
with_headers=True):
args = self._complete_args(
urls=urls, timeout=timeout, options=options, insecure=insecure,
alpn_proto=alpn_proto, force_resolve=force_resolve)
alpn_proto=alpn_proto, force_resolve=force_resolve,
with_headers=with_headers)
r = self._run(args, with_stats=with_stats)
if r.exit_code == 0:
if r.exit_code == 0 and with_headers:
self._parse_headerfile(self._headerfile, r=r)
if r.json:
r.response["json"] = r.json
@ -265,13 +311,14 @@ class CurlClient:
def _complete_args(self, urls, timeout=None, options=None,
insecure=False, force_resolve=True,
alpn_proto: Optional[str] = None):
alpn_proto: Optional[str] = None,
with_headers: bool = True):
if not isinstance(urls, list):
urls = [urls]
args = [
self._curl, "-s", "--path-as-is", "-D", self._headerfile,
]
args = [self._curl, "-s", "--path-as-is"]
if with_headers:
args.extend(["-D", self._headerfile])
if self.env.verbose > 2:
args.extend(['--trace', self._tracefile, '--trace-time'])

View file

@ -59,19 +59,41 @@ class EnvConfig:
self.config = DEF_CONFIG
# check cur and its features
self.curl = CURL
self.curl_features = []
self.curl_props = {
'version': None,
'os': None,
'features': [],
'protocols': [],
'libs': [],
'lib_versions': [],
}
self.curl_protos = []
p = subprocess.run(args=[self.curl, '-V'],
capture_output=True, text=True)
if p.returncode != 0:
assert False, f'{self.curl} -V failed with exit code: {p.returncode}'
for l in p.stdout.splitlines(keepends=False):
if l.startswith('curl '):
m = re.match(r'^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$', l)
if m:
self.curl_props['version'] = m.group('version')
self.curl_props['os'] = m.group('os')
self.curl_props['lib_versions'] = [
lib.lower() for lib in m.group('libs').split(' ')
]
self.curl_props['libs'] = [
re.sub(r'/.*', '',lib) for lib in self.curl_props['lib_versions']
]
if l.startswith('Features: '):
self.curl_features = [feat.lower() for feat in l[10:].split(' ')]
self.curl_props['features'] = [
feat.lower() for feat in l[10:].split(' ')
]
if l.startswith('Protocols: '):
self.curl_protos = [prot.lower() for prot in l[11:].split(' ')]
self.curl_props['protocols'] = [
prot.lower() for prot in l[11:].split(' ')
]
self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip())
log.error(f'nghttpx -v: {p.stdout}')
log.debug(f'nghttpx -v: {p.stdout}')
self.http_port = self.config['test']['http_port']
self.https_port = self.config['test']['https_port']
@ -81,6 +103,7 @@ class EnvConfig:
self.apxs = self.config['httpd']['apxs']
if len(self.apxs) == 0:
self.apxs = None
self._httpd_version = None
self.examples_pem = {
'key': 'xxx',
@ -110,7 +133,39 @@ class EnvConfig:
self.nghttpx = None
else:
self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip()) is not None
log.error(f'nghttpx -v: {p.stdout}')
log.debug(f'nghttpx -v: {p.stdout}')
self.caddy = self.config['caddy']['caddy']
if len(self.caddy) == 0:
self.caddy = 'caddy'
if self.caddy is not None:
try:
p = subprocess.run(args=[self.caddy, 'version'],
capture_output=True, text=True)
if p.returncode != 0:
# not a working caddy
self.caddy = None
except:
self.caddy = None
self.caddy_port = self.config['caddy']['port']
@property
def httpd_version(self):
if self._httpd_version is None and self.apxs is not None:
p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'],
capture_output=True, text=True)
if p.returncode != 0:
raise Exception(f'{self.apxs} failed to query HTTPD_VERSION: {p}')
self._httpd_version = p.stdout.strip()
return self._httpd_version
def _versiontuple(self, v):
v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v)
return tuple(map(int, v.split('.')))
def httpd_is_at_least(self, minv):
hv = self._versiontuple(self.httpd_version)
return hv >= self._versiontuple(minv)
def is_complete(self) -> bool:
return os.path.isfile(self.httpd) and \
@ -146,14 +201,46 @@ class Env:
def have_h3_server() -> bool:
return Env.CONFIG.nghttpx_with_h3
@staticmethod
def have_h2_curl() -> bool:
return 'http2' in Env.CONFIG.curl_props['features']
@staticmethod
def have_h3_curl() -> bool:
return 'http3' in Env.CONFIG.curl_features
return 'http3' in Env.CONFIG.curl_props['features']
@staticmethod
def curl_uses_lib(libname: str) -> bool:
return libname.lower() in Env.CONFIG.curl_props['libs']
@staticmethod
def curl_lib_version(libname: str) -> str:
prefix = f'{libname.lower()}/'
for lversion in Env.CONFIG.curl_props['lib_versions']:
if lversion.startswith(prefix):
return lversion[len(prefix):]
return 'unknown'
@staticmethod
def curl_os() -> bool:
return Env.CONFIG.curl_props['os']
@staticmethod
def curl_version() -> bool:
return Env.CONFIG.curl_props['version']
@staticmethod
def have_h3() -> bool:
return Env.have_h3_curl() and Env.have_h3_server()
@staticmethod
def httpd_version() -> str:
return Env.CONFIG.httpd_version
@staticmethod
def httpd_is_at_least(minv) -> bool:
return Env.CONFIG.httpd_is_at_least(minv)
def __init__(self, pytestconfig=None):
self._verbose = pytestconfig.option.verbose \
if pytestconfig is not None else 0
@ -214,6 +301,14 @@ class Env:
def h3_port(self) -> str:
return self.CONFIG.h3_port
@property
def caddy(self) -> str:
return self.CONFIG.caddy
@property
def caddy_port(self) -> str:
return self.CONFIG.caddy_port
@property
def curl(self) -> str:
return self.CONFIG.curl

View file

@ -69,22 +69,24 @@ class Httpd:
self._error_log = os.path.join(self._logs_dir, 'error_log')
self._tmp_dir = os.path.join(self._apache_dir, 'tmp')
self._mods_dir = None
if env.apxs is not None:
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
capture_output=True, text=True)
if p.returncode != 0:
raise Exception(f'{env.apxs} failed to query libexecdir: {p}')
self._mods_dir = p.stdout.strip()
else:
for md in self.COMMON_MODULES_DIRS:
if os.path.isdir(md):
self._mods_dir = md
assert env.apxs
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
capture_output=True, text=True)
if p.returncode != 0:
raise Exception(f'{env.apxs} failed to query libexecdir: {p}')
self._mods_dir = p.stdout.strip()
if self._mods_dir is None:
raise Exception(f'apache modules dir cannot be found')
if not os.path.exists(self._mods_dir):
raise Exception(f'apache modules dir does not exist: {self._mods_dir}')
self._process = None
self._rmf(self._error_log)
self._init_curltest()
@property
def docs_dir(self):
return self._docs_dir
def clear_logs(self):
self._rmf(self._error_log)
@ -213,9 +215,6 @@ class Httpd:
f'Listen {self.env.http_port}',
f'Listen {self.env.https_port}',
f'TypesConfig "{self._conf_dir}/mime.types',
# we want the quest string in a response header, so we
# can check responses more easily
f'Header set rquery "%{{QUERY_STRING}}s"',
]
conf.extend([ # plain http host for domain1
f'<VirtualHost *:{self.env.http_port}>',

View file

@ -24,15 +24,16 @@
#
###########################################################################
#
import datetime
import logging
import os
import signal
import subprocess
import time
from typing import Optional
from datetime import datetime, timedelta
from .env import Env
from .curl import CurlClient
log = logging.getLogger(__name__)
@ -43,12 +44,18 @@ class Nghttpx:
def __init__(self, env: Env):
self.env = env
self._cmd = env.nghttpx
self._pid_file = os.path.join(env.gen_dir, 'nghttpx.pid')
self._conf_file = os.path.join(env.gen_dir, 'nghttpx.conf')
self._error_log = os.path.join(env.gen_dir, 'nghttpx.log')
self._stderr = os.path.join(env.gen_dir, 'nghttpx.stderr')
self._run_dir = os.path.join(env.gen_dir, 'nghttpx')
self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid')
self._conf_file = os.path.join(self._run_dir, 'nghttpx.conf')
self._error_log = os.path.join(self._run_dir, 'nghttpx.log')
self._stderr = os.path.join(self._run_dir, 'nghttpx.stderr')
self._tmp_dir = os.path.join(self._run_dir, 'tmp')
self._process = None
self._process: Optional[subprocess.Popen] = None
self._rmf(self._pid_file)
self._rmf(self._error_log)
self._mkpath(self._run_dir)
self._write_config()
def exists(self):
return os.path.exists(self._cmd)
@ -63,10 +70,15 @@ class Nghttpx:
return self._process.returncode is None
return False
def start(self):
def start_if_needed(self):
if not self.is_running():
return self.start()
return True
def start(self, wait_live=True):
self._mkpath(self._tmp_dir)
if self._process:
self.stop()
self._write_config()
args = [
self._cmd,
f'--frontend=*,{self.env.h3_port};quic',
@ -82,31 +94,78 @@ class Nghttpx:
]
ngerr = open(self._stderr, 'a')
self._process = subprocess.Popen(args=args, stderr=ngerr)
return self._process.returncode is None
if self._process.returncode is not None:
return False
return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
def stop(self):
def stop_if_running(self):
if self.is_running():
return self.stop()
return True
def stop(self, wait_dead=True):
self._mkpath(self._tmp_dir)
if self._process:
self._process.terminate()
self._process.wait(timeout=2)
self._process = None
return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
return True
def restart(self):
self.stop()
return self.start()
def reload(self, timeout: datetime.timedelta):
def reload(self, timeout: timedelta):
if self._process:
running = self._process
self._process = None
os.kill(running.pid, signal.SIGQUIT)
self.start()
try:
log.debug(f'waiting for nghttpx({running.pid}) to exit.')
running.wait(timeout=timeout.seconds)
log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
end_wait = datetime.now() + timeout
if not self.start(wait_live=False):
self._process = running
return False
while datetime.now() < end_wait:
try:
log.debug(f'waiting for nghttpx({running.pid}) to exit.')
running.wait(2)
log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
break
except subprocess.TimeoutExpired:
log.warning(f'nghttpx({running.pid}), not shut down yet.')
os.kill(running.pid, signal.SIGQUIT)
if datetime.now() >= end_wait:
log.error(f'nghttpx({running.pid}), terminate forcefully.')
os.kill(running.pid, signal.SIGKILL)
running.terminate()
running.wait(1)
return self.wait_live(timeout=timedelta(seconds=5))
return False
def wait_dead(self, timeout: timedelta):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
while datetime.now() < try_until:
check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
r = curl.http_get(url=check_url, extra_args=['--http3-only'])
if r.exit_code != 0:
return True
except subprocess.TimeoutExpired:
log.error(f'SIGQUIT nghttpx({running.pid}), but did not shut down.')
log.debug(f'waiting for nghttpx to stop responding: {r}')
time.sleep(.1)
log.debug(f"Server still responding after {timeout}")
return False
def wait_live(self, timeout: timedelta):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
while datetime.now() < try_until:
check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
r = curl.http_get(url=check_url, extra_args=['--http3-only'])
if r.exit_code == 0:
return True
log.debug(f'waiting for nghttpx to become responsive: {r}')
time.sleep(.1)
log.error(f"Server still not responding after {timeout}")
return False
def _rmf(self, path):