Skip to content

Commit a2e1fc6

Browse files
Add --port-file flag (#942)
* Add `--port-file` flag * Use `--port-file` flag for integration tests using `get_available_port` * Use temp dir * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix `base_klass` variable related lint issues * Fix main tests * Fix integration * Use timeout when terminating proc * Skip integration on win instead of xmark Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4672dd9 commit a2e1fc6

File tree

8 files changed

+57
-17
lines changed

8 files changed

+57
-17
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,7 +2206,7 @@ To run standalone benchmark for `proxy.py`, use the following command from repo
22062206
usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless]
22072207
[--threaded] [--num-workers NUM_WORKERS]
22082208
[--local-executor LOCAL_EXECUTOR] [--backlog BACKLOG]
2209-
[--hostname HOSTNAME] [--port PORT]
2209+
[--hostname HOSTNAME] [--port PORT] [--port-file PORT_FILE]
22102210
[--unix-socket-path UNIX_SOCKET_PATH]
22112211
[--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
22122212
[--log-file LOG_FILE] [--log-format LOG_FORMAT]
@@ -2232,7 +2232,7 @@ usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless]
22322232
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
22332233
[--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]
22342234

2235-
proxy.py v2.4.0rc5.dev31+gc998255
2235+
proxy.py v2.4.0rc6.dev13+ga9b8034.d20220104
22362236

22372237
options:
22382238
-h, --help show this help message and exit
@@ -2261,6 +2261,9 @@ options:
22612261
proxy server
22622262
--hostname HOSTNAME Default: 127.0.0.1. Server IP address.
22632263
--port PORT Default: 8899. Server port.
2264+
--port-file PORT_FILE
2265+
Default: None. Save server port numbers. Useful when
2266+
using --port=0 ephemeral mode.
22642267
--unix-socket-path UNIX_SOCKET_PATH
22652268
Default: None. Unix socket path to use. When provided
22662269
--host and --port flags are ignored

proxy/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def _env_threadless_compliant() -> bool:
114114
DEFAULT_PAC_FILE = None
115115
DEFAULT_PAC_FILE_URL_PATH = b'/'
116116
DEFAULT_PID_FILE = None
117+
DEFAULT_PORT_FILE = None
117118
DEFAULT_PLUGINS: List[Any] = []
118119
DEFAULT_PORT = 8899
119120
DEFAULT_SERVER_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE

proxy/common/flag.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ def initialize(
363363
),
364364
)
365365

366+
args.port_file = cast(
367+
Optional[str], opts.get(
368+
'port_file',
369+
args.port_file,
370+
),
371+
)
372+
366373
args.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH
367374
os.makedirs(args.proxy_py_data_dir, exist_ok=True)
368375

proxy/common/plugins.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ def load(
7373
mro = list(inspect.getmro(klass))
7474
# Find the base plugin class that
7575
# this plugin_ is implementing
76-
found = False
77-
for base_klass in mro:
78-
if bytes_(base_klass.__name__) in p:
79-
found = True
76+
base_klass = None
77+
for k in mro:
78+
if bytes_(k.__name__) in p:
79+
base_klass = k
8080
break
81-
if not found:
81+
if base_klass is None:
8282
raise ValueError('%s is NOT a valid plugin' % text_(plugin_))
8383
if klass not in p[bytes_(base_klass.__name__)]:
8484
p[bytes_(base_klass.__name__)].append(klass)

proxy/core/acceptor/listener.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Optional, Any
2121

2222
from ...common.flag import flags
23-
from ...common.constants import DEFAULT_BACKLOG, DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT
23+
from ...common.constants import DEFAULT_BACKLOG, DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT, DEFAULT_PORT_FILE
2424

2525

2626
flags.add_argument(
@@ -38,10 +38,19 @@
3838
)
3939

4040
flags.add_argument(
41-
'--port', type=int, default=DEFAULT_PORT,
41+
'--port',
42+
type=int,
43+
default=DEFAULT_PORT,
4244
help='Default: 8899. Server port.',
4345
)
4446

47+
flags.add_argument(
48+
'--port-file',
49+
type=str,
50+
default=DEFAULT_PORT_FILE,
51+
help='Default: None. Save server port numbers. Useful when using --port=0 ephemeral mode.',
52+
)
53+
4554
flags.add_argument(
4655
'--unix-socket-path',
4756
type=str,

proxy/proxy.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def setup(self) -> None:
164164
# we are listening upon. This is necessary to preserve
165165
# the server port when `--port=0` is used.
166166
self.flags.port = self.listener._port
167+
self._write_port_file()
167168
# Setup EventManager
168169
if self.flags.enable_events:
169170
logger.info('Core Event enabled')
@@ -204,6 +205,7 @@ def shutdown(self) -> None:
204205
self.event_manager.shutdown()
205206
assert self.listener
206207
self.listener.shutdown()
208+
self._delete_port_file()
207209
self._delete_pid_file()
208210

209211
@property
@@ -221,13 +223,23 @@ def _delete_pid_file(self) -> None:
221223
and os.path.exists(self.flags.pid_file):
222224
os.remove(self.flags.pid_file)
223225

226+
def _write_port_file(self) -> None:
227+
if self.flags.port_file:
228+
with open(self.flags.port_file, 'wb') as port_file:
229+
port_file.write(bytes_(self.flags.port))
230+
231+
def _delete_port_file(self) -> None:
232+
if self.flags.port_file \
233+
and os.path.exists(self.flags.port_file):
234+
os.remove(self.flags.port_file)
235+
224236
def _register_signals(self) -> None:
225237
# TODO: Handle SIGINFO, SIGUSR1, SIGUSR2
226238
signal.signal(signal.SIGINT, self._handle_exit_signal)
227239
signal.signal(signal.SIGTERM, self._handle_exit_signal)
228240
if not IS_WINDOWS:
229241
signal.signal(signal.SIGHUP, self._handle_exit_signal)
230-
# TODO: SIGQUIT is ideally meant for terminate with core dumps
242+
# TODO: SIGQUIT is ideally meant to terminate with core dumps
231243
signal.signal(signal.SIGQUIT, self._handle_exit_signal)
232244

233245
@staticmethod

tests/integration/test_integration.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
1111
Test the simplest proxy use scenario for smoke.
1212
"""
13+
import time
1314
import pytest
15+
import tempfile
1416

1517
from pathlib import Path
1618
from subprocess import check_output, Popen
1719
from typing import Generator, Any
1820

1921
from proxy.common.constants import IS_WINDOWS
20-
from proxy.common.utils import get_available_port
2122

2223

2324
# FIXME: Ignore is necessary for as long as pytest hasn't figured out
@@ -34,16 +35,20 @@ def proxy_py_subprocess(request: Any) -> Generator[int, None, None]:
3435
3536
After the testing is over, tear it down.
3637
"""
37-
port = get_available_port()
38+
port_file = Path(tempfile.gettempdir()) / 'proxy.port'
3839
proxy_cmd = (
3940
'python', '-m', 'proxy',
4041
'--hostname', '127.0.0.1',
41-
'--port', str(port),
42+
'--port', '0',
43+
'--port-file', str(port_file),
4244
'--enable-web-server',
4345
) + tuple(request.param.split())
4446
proxy_proc = Popen(proxy_cmd)
47+
# Needed because port file might not be available immediately
48+
while not port_file.exists():
49+
time.sleep(1)
4550
try:
46-
yield port
51+
yield int(port_file.read_text())
4752
finally:
4853
proxy_proc.terminate()
4954
proxy_proc.wait()
@@ -64,10 +69,9 @@ def proxy_py_subprocess(request: Any) -> Generator[int, None, None]:
6469
),
6570
indirect=True,
6671
) # type: ignore[misc]
67-
@pytest.mark.xfail(
72+
@pytest.mark.skipif(
6873
IS_WINDOWS,
6974
reason='OSError: [WinError 193] %1 is not a valid Win32 application',
70-
raises=OSError,
7175
) # type: ignore[misc]
7276
def test_integration(proxy_py_subprocess: int) -> None:
7377
"""An acceptance test using ``curl`` through proxy.py."""

tests/test_main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from unittest import mock
1616

1717
from proxy.proxy import main, entry_point
18-
from proxy.common.constants import _env_threadless_compliant # noqa: WPS450
18+
from proxy.common.constants import DEFAULT_PORT_FILE, _env_threadless_compliant # noqa: WPS450
1919
from proxy.common.utils import bytes_
2020

2121
from proxy.common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_LOCAL_EXECUTOR, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE
@@ -69,6 +69,7 @@ def mock_default_args(mock_args: mock.Mock) -> None:
6969
mock_args.enable_dashboard = DEFAULT_ENABLE_DASHBOARD
7070
mock_args.work_klass = DEFAULT_WORK_KLASS
7171
mock_args.local_executor = int(DEFAULT_LOCAL_EXECUTOR)
72+
mock_args.port_file = DEFAULT_PORT_FILE
7273

7374
@mock.patch('os.remove')
7475
@mock.patch('os.path.exists')
@@ -96,6 +97,7 @@ def test_entry_point(
9697
mock_initialize.return_value.local_executor = 0
9798
mock_initialize.return_value.enable_events = False
9899
mock_initialize.return_value.pid_file = pid_file
100+
mock_initialize.return_value.port_file = None
99101
entry_point()
100102
mock_event_manager.assert_not_called()
101103
mock_listener.assert_called_once_with(
@@ -143,6 +145,7 @@ def test_main_with_no_flags(
143145
mock_sleep.side_effect = KeyboardInterrupt()
144146
mock_initialize.return_value.local_executor = 0
145147
mock_initialize.return_value.enable_events = False
148+
mock_initialize.return_value.port_file = None
146149
main()
147150
mock_event_manager.assert_not_called()
148151
mock_listener.assert_called_once_with(
@@ -183,6 +186,7 @@ def test_enable_events(
183186
mock_sleep.side_effect = KeyboardInterrupt()
184187
mock_initialize.return_value.local_executor = 0
185188
mock_initialize.return_value.enable_events = True
189+
mock_initialize.return_value.port_file = None
186190
main()
187191
mock_event_manager.assert_called_once()
188192
mock_event_manager.return_value.setup.assert_called_once()

0 commit comments

Comments
 (0)