Skip to content

Commit

Permalink
Add support for unix domain sockets (#43)
Browse files Browse the repository at this point in the history
* add support for unix domain sockets

* remove wrongly added setuptools

* add dev dependencies for unix sockets to ci

* format test for unixsocket using black

* remove unused imports

* remove debug print

* drop python3.7 support

* retry initial failure and keep tempfolder

* stop test after one successful connection
  • Loading branch information
najtin authored Oct 25, 2024
1 parent 5074cd1 commit f4bcfde
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 9 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ jobs:
fail-fast: false
matrix:
include:
- name: Test Linux py37
os: ubuntu-latest
pyversion: '3.7'
ASGI_SERVER: 'mock'
- name: Test Linux py38
os: ubuntu-latest
pyversion: '3.8'
Expand Down Expand Up @@ -106,7 +102,7 @@ jobs:
python-version: ${{ matrix.pyversion }}
- name: Install dev dependencies
run: |
pip install -U pytest pytest-cov requests websockets
pip install -U pytest pytest-cov requests websockets uvicorn hypercorn daphne
pip install .
- name: Install ASGI framework (${{ matrix.ASGI_SERVER }})
if: ${{ matrix.ASGI_SERVER != 'mock' }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ needs an ASGI server to run on, like

## Development

Extra dev dependencies: `pip install invoke pytest pytest-cov black flake8 requests websockets`
Extra dev dependencies: `pip install invoke pytest pytest-cov black flake8 requests websockets uvicorn hypercorn daphne`

Run `invoke -l` to get a list of dev commands, e.g.:

Expand Down
12 changes: 9 additions & 3 deletions asgineer/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def run(app, server, bind="localhost:8080", **kwargs):
# Check server and bind
assert isinstance(server, str), "asgineer.run() server arg must be a string."
assert isinstance(bind, str), "asgineer.run() bind arg must be a string."
assert ":" in bind, "asgineer.run() bind arg must be 'host:port'"
assert ":" in bind, (
"asgineer.run() bind arg must be 'host:port'" + "or unix:/path/to/unixsocket"
)
bind = bind.replace("localhost", "127.0.0.1")

# Select server function
Expand Down Expand Up @@ -54,7 +56,9 @@ def _run_hypercorn(appname, bind, **kwargs):
def _run_uvicorn(appname, bind, **kwargs):
from uvicorn.main import main

if ":" in bind:
if bind.startswith("unix:/"):
kwargs["uds"] = bind[5:]
elif ":" in bind:
host, _, port = bind.partition(":")
kwargs["host"] = host
kwargs["port"] = port
Expand All @@ -71,7 +75,9 @@ def _run_uvicorn(appname, bind, **kwargs):
def _run_daphne(appname, bind, **kwargs):
from daphne.cli import CommandLineInterface

if ":" in bind:
if bind.startswith("unix:/"):
kwargs["u"] = bind[5:]
elif ":" in bind:
host, _, port = bind.partition(":")
kwargs["bind"] = host
kwargs["port"] = port
Expand Down
65 changes: 65 additions & 0 deletions tests/test_unixsocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import subprocess
import tempfile
import socket
import time
import shutil
from pathlib import Path

HTTP_REQUEST = (
"GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n\r\n"
)


def test_unixsocket():
for backend_module in ["hypercorn", "uvicorn", "daphne"]:
# mkdtemp instead of normal tempdir to prevent any issues that
# might occur with early clean up
temp_folder = tempfile.mkdtemp()
socket_location = f"{temp_folder}/socket"
main_location = f"{temp_folder}/main.py"
project_location = Path(__file__).parent.parent.absolute()
code_to_run = "\n".join(
[
"# this allows us not to install asgineer and still import it",
"import importlib",
"import sys",
f"spec = importlib.util.spec_from_file_location('asgineer', '{project_location}/asgineer/__init__.py')",
"module = importlib.util.module_from_spec(spec)",
"sys.modules[spec.name] = module ",
"spec.loader.exec_module(module)",
"",
"import asgineer",
"@asgineer.to_asgi",
"async def main(request):",
' return "Ok"',
"",
"if __name__ == '__main__':",
f" asgineer.run(main, '{backend_module}', 'unix:{socket_location}')",
]
)

with open(main_location, "w") as file:
file.write(code_to_run)

process = subprocess.Popen(["python", main_location], cwd=project_location)

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
max_tries = 3
for i in range(max_tries):
time.sleep(1)
try:
client.connect(socket_location)
client.send(HTTP_REQUEST.encode())

response = client.recv(1024).decode()

if "200" in response:
break
else:
raise RuntimeError("Unexpected response")
except Exception as e:
print(repr(e))
print(f"Failed {i} times (max: {max_tries}), retrying...")

process.kill()
shutil.rmtree(temp_folder)

0 comments on commit f4bcfde

Please sign in to comment.