├── tests ├── __init__.py ├── conftest.py ├── __main__.py ├── test_libuv_api.py ├── test_runner.py ├── test_playwright.py ├── test_cython.py ├── test_executors.py ├── test_aiodns.py ├── test_testbase.py ├── test_aiomultiprocess.py ├── test_dealloc.py ├── certs │ ├── ssl_cert.pem │ └── ssl_key.pem ├── test_sourcecode.py ├── test_regr1.py ├── test_fs_event.py ├── test_process_spawning.py ├── test_aiohttp.py └── test_pipes.py ├── winloop ├── py.typed ├── .gitignore ├── includes │ ├── debug.h │ ├── debug.pxd │ ├── __init__.py │ ├── flowcontrol.pxd │ ├── consts.pxi │ ├── fork_handler.h │ ├── python.pxd │ ├── system.pxd │ ├── compat.h │ └── stdlib.pxi ├── _noop.py ├── request.pxd ├── handles │ ├── async_.pxd │ ├── check.pxd │ ├── idle.pxd │ ├── fsevent.pxd │ ├── timer.pxd │ ├── poll.pxd │ ├── udp.pxd │ ├── streamserver.pxd │ ├── tcp.pxd │ ├── pipe.pxd │ ├── basetransport.pxd │ ├── stream.pxd │ ├── async_.pyx │ ├── handle.pxd │ ├── idle.pyx │ ├── check.pyx │ ├── process.pxd │ ├── timer.pyx │ ├── fsevent.pyx │ ├── streamserver.pyx │ ├── poll.pyx │ ├── tcp.pyx │ ├── pipe.pyx │ └── basetransport.pyx ├── server.pxd ├── _version.py ├── cbhandles.pxd ├── shlex.pxd ├── request.pyx ├── errors.pyx ├── sslproto.pxd ├── server.pyx ├── pseudosock.pyx ├── lru.pyx ├── loop.pxd └── __init__.py ├── winloop.png ├── .gitmodules ├── mypy.ini ├── .github ├── dependabot.yaml ├── workflows │ ├── run-sdist.yml │ ├── build-wheels.yml │ ├── release-wheels.yml │ ├── test.yaml │ └── run-cibuildwheel.yml ├── ISSUE_TEMPLATE.md └── pull_request_template.md ├── .flake8 ├── MANIFEST.in ├── .gitignore ├── LICENSE-MIT ├── Makefile ├── examples └── bench │ ├── rlserver.py │ ├── echoclient.py │ └── echoserver.py └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /winloop/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /winloop/.gitignore: -------------------------------------------------------------------------------- 1 | *.c 2 | *.html 3 | -------------------------------------------------------------------------------- /winloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vizonex/Winloop/HEAD/winloop.png -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | multiprocessing.freeze_support() 4 | -------------------------------------------------------------------------------- /winloop/includes/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef UVLOOP_DEBUG 2 | #define UVLOOP_DEBUG 0 3 | #endif 4 | -------------------------------------------------------------------------------- /winloop/includes/debug.pxd: -------------------------------------------------------------------------------- 1 | cdef extern from "includes/debug.h": 2 | 3 | cdef int UVLOOP_DEBUG 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/libuv"] 2 | path = vendor/libuv 3 | url = https://github.com/libuv/libuv.git 4 | -------------------------------------------------------------------------------- /winloop/_noop.py: -------------------------------------------------------------------------------- 1 | def noop() -> None: 2 | """Empty function to invoke CPython ceval loop.""" 3 | return 4 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | incremental = True 3 | strict = True 4 | 5 | [mypy-winloop._testbase] 6 | ignore_errors = True 7 | -------------------------------------------------------------------------------- /winloop/request.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVRequest: 2 | cdef: 3 | uv.uv_req_t *request 4 | bint done 5 | Loop loop 6 | 7 | cdef on_done(self) 8 | cdef cancel(self) 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /winloop/handles/async_.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVAsync(UVHandle): 2 | cdef: 3 | method_t callback 4 | object ctx 5 | 6 | cdef _init(self, Loop loop, method_t callback, object ctx) 7 | 8 | cdef send(self) 9 | 10 | @staticmethod 11 | cdef UVAsync new(Loop loop, method_t callback, object ctx) 12 | -------------------------------------------------------------------------------- /winloop/handles/check.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVCheck(UVHandle): 2 | cdef: 3 | Handle h 4 | bint running 5 | 6 | # All "inline" methods are final 7 | 8 | cdef _init(self, Loop loop, Handle h) 9 | 10 | cdef inline stop(self) 11 | cdef inline start(self) 12 | 13 | @staticmethod 14 | cdef UVCheck new(Loop loop, Handle h) 15 | -------------------------------------------------------------------------------- /winloop/handles/idle.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVIdle(UVHandle): 2 | cdef: 3 | Handle h 4 | bint running 5 | 6 | # All "inline" methods are final 7 | 8 | cdef _init(self, Loop loop, Handle h) 9 | 10 | cdef inline stop(self) 11 | cdef inline start(self) 12 | 13 | @staticmethod 14 | cdef UVIdle new(Loop loop, Handle h) 15 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | filename = *.py,*.pyi 3 | ignore = E402,E731,D100,D101,D102,D103,D104,D105,W503,W504,E252 4 | exclude = .git,__pycache__,build,dist,.eggs,postgres,vendor 5 | 6 | per-file-ignores = 7 | *.pyx,*.pxd,*.pxi: E211, E222, E225, E226, E227, E999 8 | *.pyi: F401, F403, F405, F811, E127, E128, E203, E266, E301, E302, E305, E501, E701, E704, E741, B303, W503, W504 9 | -------------------------------------------------------------------------------- /winloop/handles/fsevent.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVFSEvent(UVHandle): 2 | cdef: 3 | object callback 4 | bint running 5 | 6 | cdef _init(self, Loop loop, object callback, object context) 7 | cdef _close(self) 8 | cdef start(self, char* path, int flags) 9 | cdef stop(self) 10 | 11 | @staticmethod 12 | cdef UVFSEvent new(Loop loop, object callback, object context) 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include docs *.py *.rst 2 | recursive-include examples *.py 3 | recursive-include tests *.py *.pem 4 | recursive-include winloop *.pyx *.pxd *.pxi *.py *.c *.h *.pyi py.typed 5 | recursive-include vendor/libuv * 6 | recursive-exclude vendor/libuv/.git * 7 | recursive-exclude vendor/libuv/docs * 8 | recursive-exclude vendor/libuv/img * 9 | include LICENSE-MIT LICENSE-APACHE README.md Makefile winloop.png .flake8 mypy.ini 10 | -------------------------------------------------------------------------------- /winloop/includes/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | # These have to be synced with the stdlib.pxi 4 | import asyncio 5 | import collections 6 | import concurrent.futures 7 | import errno 8 | import functools 9 | import gc 10 | import inspect 11 | import itertools 12 | import os 13 | import signal 14 | import socket 15 | import ssl 16 | import stat 17 | import subprocess 18 | import sys 19 | import threading 20 | import time 21 | import traceback 22 | import warnings 23 | import weakref 24 | -------------------------------------------------------------------------------- /winloop/server.pxd: -------------------------------------------------------------------------------- 1 | cdef class Server: 2 | cdef: 3 | list _servers 4 | list _waiters 5 | int _active_count 6 | Loop _loop 7 | bint _serving 8 | object _serving_forever_fut 9 | object __weakref__ 10 | 11 | cdef _add_server(self, UVStreamServer srv) 12 | cdef _start_serving(self) 13 | cdef _wakeup(self) 14 | 15 | cdef _attach(self) 16 | cdef _detach(self) 17 | 18 | cdef _ref(self) 19 | cdef _unref(self) 20 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | import unittest 4 | import unittest.runner 5 | import multiprocessing 6 | 7 | 8 | def suite(): 9 | test_loader = unittest.TestLoader() 10 | test_suite = test_loader.discover(os.path.dirname(__file__)) 11 | return test_suite 12 | 13 | 14 | if __name__ == "__main__": 15 | multiprocessing.freeze_support() 16 | runner = unittest.runner.TextTestRunner() 17 | result = runner.run(suite()) 18 | sys.exit(not result.wasSuccessful()) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *._* 2 | *.pyc 3 | *.pyo 4 | # windows binaries 5 | *.pyd 6 | *.ymlc 7 | *.ymlc~ 8 | *.scssc 9 | *.so 10 | *~ 11 | .#* 12 | .DS_Store 13 | .project 14 | .pydevproject 15 | .settings 16 | .idea 17 | /.ropeproject 18 | \#*# 19 | /pub 20 | /test*.py 21 | /.local 22 | /perf.data* 23 | /config_local.yml 24 | /build 25 | __pycache__/ 26 | .d8_history 27 | /*.egg 28 | /*.egg-info 29 | /dist 30 | /.cache 31 | docs/_build 32 | uvloop/loop.*.pyd 33 | /.pytest_cache/ 34 | /.mypy_cache/ 35 | /.vscode 36 | /.eggs 37 | /.venv* 38 | /wheelhouse 39 | /uvloop-dev 40 | -------------------------------------------------------------------------------- /winloop/handles/timer.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVTimer(UVHandle): 2 | cdef: 3 | method_t callback 4 | object ctx 5 | bint running 6 | uint64_t timeout 7 | uint64_t start_t 8 | 9 | cdef _init(self, Loop loop, method_t callback, object ctx, 10 | uint64_t timeout) 11 | 12 | cdef stop(self) 13 | cdef start(self) 14 | cdef get_when(self) 15 | 16 | @staticmethod 17 | cdef UVTimer new(Loop loop, method_t callback, object ctx, 18 | uint64_t timeout) 19 | -------------------------------------------------------------------------------- /winloop/includes/flowcontrol.pxd: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | 4 | cdef inline add_flowcontrol_defaults(high, low, int kb): 5 | cdef int h, l 6 | if high is None: 7 | if low is None: 8 | h = kb * 1024 9 | else: 10 | l = low 11 | h = 4 * l 12 | else: 13 | h = high 14 | if low is None: 15 | l = h // 4 16 | else: 17 | l = low 18 | 19 | if not h >= l >= 0: 20 | raise ValueError('high (%r) must be >= low (%r) must be >= 0' % 21 | (h, l)) 22 | 23 | return h, l 24 | -------------------------------------------------------------------------------- /winloop/_version.py: -------------------------------------------------------------------------------- 1 | # This file MUST NOT contain anything but the __version__ assignment. 2 | # 3 | # When making a release, change the value of __version__ 4 | # to an appropriate value, and open a pull request against 5 | # the correct branch (master if making a new feature release). 6 | # The commit message MUST contain a properly formatted release 7 | # log, and the commit must be signed. 8 | # 9 | # The release automation will: build and test the packages for the 10 | # supported platforms, publish the packages on PyPI, merge the PR 11 | # to the target branch, create a Git tag pointing to the commit. 12 | 13 | __version__ = "0.4.0" 14 | -------------------------------------------------------------------------------- /winloop/handles/poll.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVPoll(UVHandle): 2 | cdef: 3 | int fd 4 | Handle reading_handle 5 | Handle writing_handle 6 | 7 | cdef _init(self, Loop loop, int fd) 8 | cdef _close(self) 9 | 10 | cdef inline _poll_start(self, int flags) 11 | cdef inline _poll_stop(self) 12 | 13 | cdef int is_active(self) noexcept 14 | 15 | cdef is_reading(self) 16 | cdef is_writing(self) 17 | 18 | cdef start_reading(self, Handle callback) 19 | cdef start_writing(self, Handle callback) 20 | cdef stop_reading(self) 21 | cdef stop_writing(self) 22 | cdef stop(self) 23 | 24 | @staticmethod 25 | cdef UVPoll new(Loop loop, int fd) 26 | -------------------------------------------------------------------------------- /winloop/handles/udp.pxd: -------------------------------------------------------------------------------- 1 | cdef class UDPTransport(UVBaseTransport): 2 | cdef: 3 | bint __receiving 4 | int _family 5 | object _address 6 | 7 | cdef _init(self, Loop loop, unsigned int family) 8 | cdef _set_address(self, system.addrinfo *addr) 9 | 10 | cdef _connect(self, system.sockaddr* addr, size_t addr_len) 11 | 12 | cdef _bind(self, system.sockaddr* addr) 13 | cdef open(self, int family, int sockfd) 14 | cdef _set_broadcast(self, bint on) 15 | 16 | cdef inline __receiving_started(self) 17 | cdef inline __receiving_stopped(self) 18 | 19 | cdef _send(self, object data, object addr) 20 | 21 | cdef _on_receive(self, bytes data, object exc, object addr) 22 | cdef _on_sent(self, object exc, object context=*) 23 | -------------------------------------------------------------------------------- /.github/workflows/run-sdist.yml: -------------------------------------------------------------------------------- 1 | # borrowed from cyares/pycares 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build_sdist: 8 | name: Build source distribution 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | with: 13 | submodules: true 14 | - uses: actions/setup-python@v6 15 | name: Install Python 16 | with: 17 | check-latest: true 18 | python-version: '3.13' 19 | - name: Install setuptools 20 | run: python -m pip install -U setuptools 21 | - name: Install Cython 22 | run : python -m pip install cython 23 | - name: Build sdist 24 | run: python setup.py sdist 25 | - uses: actions/upload-artifact@v6 26 | with: 27 | name: sdist 28 | path: dist/*.tar.gz 29 | -------------------------------------------------------------------------------- /.github/workflows/build-wheels.yml: -------------------------------------------------------------------------------- 1 | # Borrowed from pycares & cyares, 2 | # this will be the nightly build I've 3 | # been talking about adding for the past 2 years, 4 | # - Vizonex 5 | 6 | name: Build Wheels 7 | 8 | on: [pull_request] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build_wheels: 16 | uses: ./.github/workflows/run-cibuildwheel.yml 17 | with: 18 | prerelease-pythons: true 19 | 20 | build_sdist: 21 | uses: ./.github/workflows/run-sdist.yml 22 | 23 | check_build: 24 | needs: [build_wheels, build_sdist] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/download-artifact@v7 28 | with: 29 | path: dist 30 | merge-multiple: true 31 | - run: ls -lR dist -------------------------------------------------------------------------------- /tests/test_libuv_api.py: -------------------------------------------------------------------------------- 1 | from winloop import _testbase as tb 2 | from winloop.loop import _testhelper_unwrap_capsuled_pointer as unwrap 3 | from winloop.loop import libuv_get_loop_t_ptr, libuv_get_version 4 | 5 | 6 | class Test_UV_libuv(tb.UVTestCase): 7 | def test_libuv_get_loop_t_ptr(self): 8 | loop1 = self.new_loop() 9 | cap1 = libuv_get_loop_t_ptr(loop1) 10 | cap2 = libuv_get_loop_t_ptr(loop1) 11 | 12 | loop2 = self.new_loop() 13 | cap3 = libuv_get_loop_t_ptr(loop2) 14 | 15 | try: 16 | self.assertEqual(unwrap(cap1), unwrap(cap2)) 17 | self.assertNotEqual(unwrap(cap1), unwrap(cap3)) 18 | finally: 19 | loop1.close() 20 | loop2.close() 21 | 22 | def test_libuv_get_version(self): 23 | self.assertGreater(libuv_get_version(), 0) 24 | -------------------------------------------------------------------------------- /winloop/handles/streamserver.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVStreamServer(UVSocketHandle): 2 | cdef: 3 | int backlog 4 | object ssl 5 | object ssl_handshake_timeout 6 | object ssl_shutdown_timeout 7 | object protocol_factory 8 | bint opened 9 | Server _server 10 | 11 | # All "inline" methods are final 12 | 13 | cdef inline _init(self, Loop loop, object protocol_factory, 14 | Server server, 15 | object backlog, 16 | object ssl, 17 | object ssl_handshake_timeout, 18 | object ssl_shutdown_timeout) 19 | 20 | cdef inline _mark_as_open(self) 21 | 22 | cdef inline listen(self) 23 | cdef inline _on_listen(self) 24 | 25 | cdef UVStream _make_new_transport(self, object protocol, object waiter, 26 | object context) 27 | -------------------------------------------------------------------------------- /.github/workflows/release-wheels.yml: -------------------------------------------------------------------------------- 1 | # Borrowed from pycares/cyares 2 | 3 | name: Release Wheels 4 | 5 | on: 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | build_wheels: 12 | uses: ./.github/workflows/run-cibuildwheel.yml 13 | with: 14 | fail-fast: true 15 | 16 | build_sdist: 17 | uses: ./.github/workflows/run-sdist.yml 18 | 19 | upload_pypi: 20 | needs: [build_wheels, build_sdist] 21 | if: github.event_name == 'release' && github.event.action == 'published' 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/download-artifact@v7 25 | with: 26 | path: dist 27 | merge-multiple: true 28 | - run: ls -lR dist 29 | - name: Upload to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | user: __token__ 33 | # TODO: Move to trusted Publishing after winloop 0.1.91 34 | password: ${{ secrets.PYPI_PASSWORD }} 35 | -------------------------------------------------------------------------------- /winloop/cbhandles.pxd: -------------------------------------------------------------------------------- 1 | cdef class Handle: 2 | cdef: 3 | Loop loop 4 | object context 5 | bint _cancelled 6 | 7 | str meth_name 8 | int cb_type 9 | void *callback 10 | object arg1, arg2, arg3, arg4 11 | 12 | object __weakref__ 13 | 14 | readonly _source_traceback # type: ignore 15 | 16 | cdef inline _set_loop(self, Loop loop) 17 | cdef inline _set_context(self, object context) 18 | 19 | cdef inline _run(self) 20 | cdef _cancel(self) 21 | 22 | cdef _format_handle(self) 23 | 24 | 25 | cdef class TimerHandle: 26 | cdef: 27 | object callback 28 | tuple args 29 | bint _cancelled 30 | UVTimer timer 31 | Loop loop 32 | object context 33 | tuple _debug_info 34 | object __weakref__ 35 | object _when 36 | 37 | cdef _run(self) 38 | cdef _cancel(self) 39 | cdef inline _clear(self) 40 | -------------------------------------------------------------------------------- /winloop/handles/tcp.pxd: -------------------------------------------------------------------------------- 1 | cdef class TCPServer(UVStreamServer): 2 | cdef bind(self, system.sockaddr* addr, unsigned int flags=*) 3 | 4 | @staticmethod 5 | cdef TCPServer new(Loop loop, object protocol_factory, Server server, 6 | unsigned int flags, 7 | object backlog, 8 | object ssl, 9 | object ssl_handshake_timeout, 10 | object ssl_shutdown_timeout) 11 | 12 | 13 | cdef class TCPTransport(UVStream): 14 | cdef: 15 | bint __peername_set 16 | bint __sockname_set 17 | system.sockaddr_storage __peername 18 | system.sockaddr_storage __sockname 19 | 20 | cdef bind(self, system.sockaddr* addr, unsigned int flags=*) 21 | cdef connect(self, system.sockaddr* addr) 22 | cdef _set_nodelay(self) 23 | 24 | @staticmethod 25 | cdef TCPTransport new(Loop loop, object protocol, Server server, 26 | object waiter, object context) 27 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | import winloop as uvloop 5 | 6 | 7 | class TestSourceCode(unittest.TestCase): 8 | def test_uvloop_run_1(self): 9 | CNT = 0 10 | 11 | async def main(): 12 | nonlocal CNT 13 | CNT += 1 14 | 15 | loop = asyncio.get_running_loop() 16 | 17 | self.assertTrue(isinstance(loop, uvloop.Loop)) 18 | self.assertTrue(loop.get_debug()) 19 | 20 | return "done" 21 | 22 | result = uvloop.run(main(), debug=True) 23 | 24 | self.assertEqual(result, "done") 25 | self.assertEqual(CNT, 1) 26 | 27 | def test_uvloop_run_2(self): 28 | async def main(): 29 | pass 30 | 31 | coro = main() 32 | with self.assertRaisesRegex(TypeError, " a non-winloop event loop"): 33 | uvloop.run( 34 | coro, 35 | loop_factory=asyncio.DefaultEventLoopPolicy().new_event_loop, 36 | ) 37 | 38 | coro.close() 39 | -------------------------------------------------------------------------------- /winloop/handles/pipe.pxd: -------------------------------------------------------------------------------- 1 | cdef class UnixServer(UVStreamServer): 2 | 3 | cdef bind(self, str path) 4 | 5 | @staticmethod 6 | cdef UnixServer new(Loop loop, object protocol_factory, Server server, 7 | object backlog, 8 | object ssl, 9 | object ssl_handshake_timeout, 10 | object ssl_shutdown_timeout) 11 | 12 | 13 | cdef class UnixTransport(UVStream): 14 | 15 | @staticmethod 16 | cdef UnixTransport new(Loop loop, object protocol, Server server, 17 | object waiter, object context) 18 | 19 | cdef connect(self, char* addr) 20 | 21 | 22 | cdef class ReadUnixTransport(UVStream): 23 | 24 | @staticmethod 25 | cdef ReadUnixTransport new(Loop loop, object protocol, Server server, 26 | object waiter) 27 | 28 | 29 | cdef class WriteUnixTransport(UVStream): 30 | 31 | @staticmethod 32 | cdef WriteUnixTransport new(Loop loop, object protocol, Server server, 33 | object waiter) 34 | -------------------------------------------------------------------------------- /winloop/includes/consts.pxi: -------------------------------------------------------------------------------- 1 | cdef enum: 2 | 3 | # There is some Good News, Cython plans not to deprecate 4 | # DEF Constant Macros for now lets keep these enums but we will revert later.. 5 | UV_STREAM_RECV_BUF_SIZE = 256000 # 250kb 6 | 7 | FLOW_CONTROL_HIGH_WATER = 64 # KiB 8 | FLOW_CONTROL_HIGH_WATER_SSL_READ = 256 # KiB 9 | FLOW_CONTROL_HIGH_WATER_SSL_WRITE = 512 # KiB 10 | 11 | DEFAULT_FREELIST_SIZE = 250 12 | DNS_PYADDR_TO_SOCKADDR_CACHE_SIZE = 2048 13 | 14 | DEBUG_STACK_DEPTH = 10 15 | 16 | __PROCESS_DEBUG_SLEEP_AFTER_FORK = 1 17 | 18 | LOG_THRESHOLD_FOR_CONNLOST_WRITES = 5 19 | SSL_READ_MAX_SIZE = 256 * 1024 20 | 21 | 22 | cdef extern from *: 23 | ''' 24 | // Number of seconds to wait for SSL handshake to complete 25 | // The default timeout matches that of Nginx. 26 | #define SSL_HANDSHAKE_TIMEOUT 60.0 27 | 28 | // Number of seconds to wait for SSL shutdown to complete 29 | // The default timeout mimics lingering_time 30 | #define SSL_SHUTDOWN_TIMEOUT 30.0 31 | ''' 32 | 33 | const float SSL_HANDSHAKE_TIMEOUT 34 | const float SSL_SHUTDOWN_TIMEOUT 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2023-present Vizonex , winloop authors and the winloop contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ## ISSUE 13 | 15 | 16 | 17 | - [ ] bug 18 | - [ ] optimization idea 19 | 20 | ## TO REPRODUCE 21 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /winloop/shlex.pxd: -------------------------------------------------------------------------------- 1 | 2 | 3 | # TODO: In the future I would like to see this module start using 4 | # const char* and other simillar optimizations, will start with 5 | # this for now - Vizonex 6 | 7 | cdef class shlex: 8 | cdef: 9 | readonly str commenters 10 | readonly str wordchars 11 | readonly str whitespace 12 | readonly str escape 13 | readonly str quotes 14 | readonly str escapedquotes 15 | readonly bint whitespace_split 16 | readonly str infile 17 | readonly object instream 18 | readonly str source 19 | readonly int debug 20 | readonly int lineno 21 | readonly str token 22 | readonly str eof 23 | readonly bint posix 24 | object _punctuation_chars 25 | object state 26 | object pushback 27 | object filestack 28 | 29 | cpdef object get_token(self) 30 | cpdef object push_token(self, object tok) 31 | cpdef object read_token(self) 32 | cpdef tuple sourcehook(self, str newfile) 33 | cpdef object push_source(self, object newstream, object newfile=*) 34 | cpdef object pop_source(self) 35 | cpdef object error_leader(self, object infile=*, object lineno=*) 36 | 37 | # Custom function used for saving time with splitting data up. 38 | cpdef list split(self, str s) 39 | -------------------------------------------------------------------------------- /tests/test_playwright.py: -------------------------------------------------------------------------------- 1 | try: 2 | from playwright.async_api import async_playwright 3 | except ImportError: 4 | skip_tests = True 5 | else: 6 | skip_tests = False 7 | 8 | import unittest 9 | 10 | from winloop import _testbase as tb 11 | 12 | # SEE: https://github.com/Vizonex/Winloop/issues/34 13 | 14 | 15 | class _TestPlaywright: 16 | def test_example_code(self): 17 | # Example code is from microsoft's own example, 18 | # see https://github.com/microsoft/playwright-python 19 | async def run(): 20 | async with async_playwright() as p: 21 | for browser_type in [p.chromium, p.firefox, p.webkit]: 22 | browser = await browser_type.launch() 23 | page = await browser.new_page() 24 | await page.goto("http://playwright.dev") 25 | path = f"example-{browser_type.name}.png" 26 | await page.screenshot(path=path) 27 | await browser.close() 28 | 29 | self.loop.run_until_complete(run()) 30 | 31 | 32 | @unittest.skipIf(skip_tests, "no playwright module") 33 | class Test_UV_Playwright(_TestPlaywright, tb.UVTestCase): 34 | pass 35 | 36 | 37 | @unittest.skipIf(skip_tests, "no playwright module") 38 | class Test_AIO_Playwright(_TestPlaywright, tb.AIOTestCase): 39 | pass 40 | -------------------------------------------------------------------------------- /winloop/includes/fork_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef UVLOOP_FORK_HANDLER_H_ 2 | #define UVLOOP_FORK_HANDLER_H_ 3 | 4 | #ifndef _WIN32 5 | #include 6 | #endif 7 | 8 | volatile uint64_t MAIN_THREAD_ID = 0; 9 | volatile int8_t MAIN_THREAD_ID_SET = 0; 10 | 11 | typedef void (*OnForkHandler)(void); 12 | 13 | OnForkHandler __forkHandler = NULL; 14 | 15 | /* Auxiliary function to call global fork handler if defined. 16 | 17 | Note: Fork handler needs to be in C (not cython) otherwise it would require 18 | GIL to be present, but some forks can exec non-python processes. 19 | */ 20 | void handleAtFork(void) { 21 | // Reset the MAIN_THREAD_ID on fork, because the main thread ID is not 22 | // always the same after fork, especially when forked from within a thread. 23 | MAIN_THREAD_ID_SET = 0; 24 | 25 | if (__forkHandler != NULL) { 26 | __forkHandler(); 27 | } 28 | } 29 | 30 | 31 | void setForkHandler(OnForkHandler handler) 32 | { 33 | __forkHandler = handler; 34 | } 35 | 36 | 37 | void resetForkHandler(void) 38 | { 39 | __forkHandler = NULL; 40 | } 41 | 42 | void setMainThreadID(uint64_t id) { 43 | MAIN_THREAD_ID = id; 44 | MAIN_THREAD_ID_SET = 1; 45 | } 46 | 47 | #ifdef _WIN32 48 | int pthread_atfork( 49 | void (*prepare)(), 50 | void (*parent)(), 51 | void (*child)()) { 52 | return 0; 53 | } 54 | #endif 55 | 56 | #endif -------------------------------------------------------------------------------- /tests/test_cython.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from winloop._testbase import UVTestCase 4 | 5 | 6 | class TestCythonIntegration(UVTestCase): 7 | def test_cython_coro_is_coroutine(self): 8 | from asyncio.coroutines import _format_coroutine 9 | 10 | from winloop.loop import _test_coroutine_1 11 | 12 | coro = _test_coroutine_1() 13 | 14 | coro_fmt = _format_coroutine(coro) 15 | self.assertTrue( 16 | coro_fmt.startswith("_test_coroutine_1() done") 17 | or coro_fmt.startswith("_test_coroutine_1() running") 18 | ) 19 | self.assertEqual(_test_coroutine_1.__qualname__, "_test_coroutine_1") 20 | self.assertEqual(_test_coroutine_1.__name__, "_test_coroutine_1") 21 | self.assertTrue(asyncio.iscoroutine(coro)) 22 | fut = asyncio.ensure_future(coro) 23 | self.assertTrue(isinstance(fut, asyncio.Future)) 24 | self.assertTrue(isinstance(fut, asyncio.Task)) 25 | fut.cancel() 26 | 27 | with self.assertRaises(asyncio.CancelledError): 28 | self.loop.run_until_complete(fut) 29 | 30 | try: 31 | _format_coroutine(coro) # This line checks against Cython segfault 32 | except TypeError: 33 | # TODO: Fix Cython to not reset __name__/__qualname__ to None 34 | pass 35 | coro.close() 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: _default clean clean-libuv distclean compile debug docs test testinstalled release setup-build ci-clean 2 | 3 | 4 | PYTHON ?= python 5 | ROOT = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 6 | 7 | 8 | _default: compile 9 | 10 | 11 | clean: 12 | rm -fr dist/ doc/_build/ *.egg-info winloop/loop.*.pyd uvloop/loop_d.*.pyd 13 | rm -fr winloop/*.c winloop/*.html winloop/*.so 14 | rm -fr winloop/handles/*.html winloop/includes/*.html 15 | find . -name '__pycache__' | xargs rm -rf 16 | 17 | 18 | ci-clean: clean 19 | rm -fr build/lib.* build/temp.* build/libuv 20 | 21 | 22 | clean-libuv: 23 | (cd vendor/libuv; git clean -dfX) 24 | 25 | 26 | distclean: clean clean-libuv 27 | rm -fr build/ 28 | 29 | 30 | setup-build: 31 | $(PYTHON) setup.py build_ext --inplace --cython-always 32 | 33 | 34 | compile: clean setup-build 35 | 36 | 37 | debug: clean 38 | $(PYTHON) setup.py build_ext --inplace --debug \ 39 | --cython-always \ 40 | --cython-annotate \ 41 | --cython-directives="linetrace=True" \ 42 | --define UVLOOP_DEBUG,CYTHON_TRACE,CYTHON_TRACE_NOGIL 43 | 44 | 45 | docs: 46 | $(PYTHON) setup.py build_ext --inplace build_sphinx 47 | 48 | 49 | test: 50 | PYTHONASYNCIODEBUG=1 $(PYTHON) -m unittest discover -v tests 51 | $(PYTHON) -m unittest discover -v tests 52 | 53 | 54 | testinstalled: 55 | cd "$${HOME}" && $(PYTHON) -m unittest discover -v $(ROOT)/tests 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # NOTE I'm borrowing from pycares & cyares so that we don't have to reinvent the wheel. 2 | name: Test 3 | on: [pull_request, push] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | 17 | os: [windows-latest, windows-11-arm] 18 | python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] 19 | exclude: 20 | - os: windows-11-arm # setup-python action only supports 3.11+ 21 | python-version: '3.10' 22 | steps: 23 | - uses: actions/checkout@v6 24 | with: 25 | submodules: true 26 | - name: Setup Python 27 | uses: actions/setup-python@v6 28 | with: 29 | check-latest: true 30 | python-version: ${{ matrix.python-version }} 31 | - name: Install Requirements 32 | run: python -m pip install --upgrade setuptools wheel pip cython pytest aiohttp aiodns 33 | 34 | - name: Compile Winloop 35 | run: pip install -e . 36 | 37 | - name: run-tests 38 | env: 39 | COLOR: yes 40 | PIP_USER: 1 41 | run: pytest -vv 42 | shell: bash 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /winloop/includes/python.pxd: -------------------------------------------------------------------------------- 1 | from cpython.object cimport PyObject 2 | 3 | 4 | cdef extern from "Python.h": 5 | int PY_VERSION_HEX 6 | 7 | unicode PyUnicode_FromString(const char *) 8 | 9 | void* PyMem_RawMalloc(size_t n) nogil 10 | void* PyMem_RawRealloc(void *p, size_t n) nogil 11 | void* PyMem_RawCalloc(size_t nelem, size_t elsize) nogil 12 | void PyMem_RawFree(void *p) nogil 13 | 14 | object PyUnicode_EncodeFSDefault(object) 15 | void PyErr_SetInterrupt() nogil 16 | 17 | object PyMemoryView_FromMemory(char *mem, ssize_t size, int flags) 18 | object PyMemoryView_FromObject(object obj) 19 | int PyMemoryView_Check(object obj) 20 | 21 | cdef enum: 22 | PyBUF_WRITE 23 | # This is For noop._noop to optimize the time calling it 24 | PyObject* PyObject_CallNoArgs(object func) 25 | 26 | 27 | 28 | 29 | cdef extern from "includes/compat.h": 30 | object Context_CopyCurrent() 31 | int Context_Enter(object) except -1 32 | int Context_Exit(object) except -1 33 | 34 | void PyOS_BeforeFork() 35 | void PyOS_AfterFork_Parent() 36 | void PyOS_AfterFork_Child() 37 | 38 | void _Py_RestoreSignals() 39 | 40 | # TODO: Might consider our own version or duplicates 41 | # of _PyEval_EvalFrameDefault() so we can stop using noop._noop 42 | # which has been the only problem with compiling with pyinstaller currently... 43 | # This way, no need for hooks! 44 | -------------------------------------------------------------------------------- /tests/test_executors.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import concurrent.futures 3 | import multiprocessing 4 | import unittest 5 | 6 | from winloop import _testbase as tb 7 | 8 | 9 | def fib(n): 10 | if n < 2: 11 | return 1 12 | return fib(n - 2) + fib(n - 1) 13 | 14 | 15 | class _TestExecutors: 16 | def run_pool_test(self, pool_factory): 17 | async def run(): 18 | pool = pool_factory() 19 | with pool: 20 | coros = [] 21 | for i in range(0, 10): 22 | coros.append(self.loop.run_in_executor(pool, fib, i)) 23 | res = await asyncio.gather(*coros) 24 | self.assertEqual(res, fib10) 25 | await asyncio.sleep(0.01) 26 | 27 | fib10 = [fib(i) for i in range(10)] 28 | self.loop.run_until_complete(run()) 29 | 30 | @unittest.skipIf( 31 | multiprocessing.get_start_method(False) == "spawn", 32 | "no need to test on Windows and macOS where spawn is used instead of fork", 33 | ) 34 | def test_executors_process_pool_01(self): 35 | self.run_pool_test(concurrent.futures.ProcessPoolExecutor) 36 | 37 | def test_executors_process_pool_02(self): 38 | self.run_pool_test(concurrent.futures.ThreadPoolExecutor) 39 | 40 | 41 | class TestUVExecutors(_TestExecutors, tb.UVTestCase): 42 | pass 43 | 44 | 45 | class TestAIOExecutors(_TestExecutors, tb.AIOTestCase): 46 | pass 47 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | ## What do these changes do? 7 | 8 | 9 | 10 | ## Are there changes in behavior for the user? 11 | 12 | 13 | 14 | ## Is it a substantial burden for the maintainers to support this? 15 | 16 | 27 | 28 | ## Related issue number 29 | 30 | 31 | 32 | 33 | ## Checklist 34 | 36 | 37 | - [ ] I think the code is well written 38 | - [ ] Unit tests for the changes exist 39 | - [ ] Documentation reflects the changes 40 | -------------------------------------------------------------------------------- /tests/test_aiodns.py: -------------------------------------------------------------------------------- 1 | # NOTE: I forked the aiodns Repository you will have to use that for now 2 | # or wait for the pull request I recently made to go through: 3 | # https://github.com/saghul/aiodns/pull/116 4 | 5 | try: 6 | import aiodns 7 | except ImportError: 8 | skip_tests = True 9 | else: 10 | skip_tests = False 11 | 12 | import asyncio 13 | import sys 14 | import unittest 15 | 16 | from winloop import _testbase as tb 17 | 18 | 19 | class _TestAiodns: 20 | def test_dns_query(self): 21 | # This is not allowed to fail... 22 | resolver = aiodns.DNSResolver(loop=self.loop) 23 | 24 | async def test(): 25 | async def query(name, query_type): 26 | return await resolver.query(name, query_type) 27 | 28 | await query("google.com", "A") 29 | await query("httpbin.org", "A") 30 | await query("example.com", "A") 31 | 32 | self.loop.run_until_complete(test()) 33 | 34 | 35 | @unittest.skipIf(skip_tests, "no aiodns module") 36 | class Test_UV_Aiodns(_TestAiodns, tb.UVTestCase): 37 | pass 38 | 39 | 40 | @unittest.skipIf(skip_tests, "no aiodns module") 41 | class Test_AIO_Aiodns(_TestAiodns, tb.AIOTestCase): 42 | # Winloop comment: switching to selector loop (instead of proactor), 43 | # see https://github.com/saghul/aiodns/issues/86 44 | if sys.platform == "win32": 45 | 46 | def new_policy(self): 47 | return asyncio.WindowsSelectorEventLoopPolicy() 48 | -------------------------------------------------------------------------------- /tests/test_testbase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from winloop import _testbase as tb 4 | 5 | 6 | class TestBaseTest(unittest.TestCase): 7 | def test_duplicate_methods(self): 8 | with self.assertRaisesRegex(RuntimeError, "duplicate test Foo.test_a"): 9 | 10 | class Foo(tb.BaseTestCase): 11 | def test_a(self): 12 | pass 13 | 14 | def test_b(self): 15 | pass 16 | 17 | def test_a(self): # NOQA 18 | pass 19 | 20 | def test_duplicate_methods_parent_1(self): 21 | class FooBase: 22 | def test_a(self): 23 | pass 24 | 25 | with self.assertRaisesRegex( 26 | RuntimeError, "duplicate test Foo.test_a.*defined in FooBase" 27 | ): 28 | 29 | class Foo(FooBase, tb.BaseTestCase): 30 | def test_b(self): 31 | pass 32 | 33 | def test_a(self): 34 | pass 35 | 36 | def test_duplicate_methods_parent_2(self): 37 | class FooBase(tb.BaseTestCase): 38 | def test_a(self): 39 | pass 40 | 41 | with self.assertRaisesRegex( 42 | RuntimeError, "duplicate test Foo.test_a.*defined in FooBase" 43 | ): 44 | 45 | class Foo(FooBase): 46 | def test_b(self): 47 | pass 48 | 49 | def test_a(self): 50 | pass 51 | -------------------------------------------------------------------------------- /winloop/handles/basetransport.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVBaseTransport(UVSocketHandle): 2 | 3 | cdef: 4 | readonly bint _closing 5 | 6 | bint _protocol_connected 7 | bint _protocol_paused 8 | object _protocol_data_received 9 | size_t _high_water 10 | size_t _low_water 11 | 12 | object _protocol 13 | Server _server 14 | object _waiter 15 | 16 | dict _extra_info 17 | 18 | uint32_t _conn_lost 19 | 20 | object __weakref__ 21 | 22 | # All "inline" methods are final 23 | 24 | cdef inline _maybe_pause_protocol(self) 25 | cdef inline _maybe_resume_protocol(self) 26 | 27 | cdef inline _schedule_call_connection_made(self) 28 | cdef inline _schedule_call_connection_lost(self, exc) 29 | 30 | cdef _wakeup_waiter(self) 31 | cdef _call_connection_made(self) 32 | cdef _call_connection_lost(self, exc) 33 | 34 | # Overloads of UVHandle methods: 35 | cdef _fatal_error(self, exc, throw, reason=?) 36 | cdef _close(self) 37 | 38 | cdef inline _set_server(self, Server server) 39 | cdef inline _set_waiter(self, object waiter) 40 | 41 | cdef _set_protocol(self, object protocol) 42 | cdef _clear_protocol(self) 43 | 44 | cdef inline _init_protocol(self) 45 | cdef inline _add_extra_info(self, str name, object obj) 46 | 47 | # === overloads === 48 | 49 | cdef _new_socket(self) 50 | cdef size_t _get_write_buffer_size(self) 51 | 52 | cdef bint _is_reading(self) 53 | cdef _start_reading(self) 54 | cdef _stop_reading(self) 55 | -------------------------------------------------------------------------------- /.github/workflows/run-cibuildwheel.yml: -------------------------------------------------------------------------------- 1 | # Borrowing from pycares/cyares 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | fail-fast: 7 | description: Whether the wheel build should stop and fail as soon as any job fails. 8 | required: false 9 | default: false 10 | type: boolean 11 | prerelease-pythons: 12 | description: Whether the wheels should be built for pre-release Pythons that are not ABI stable yet. 13 | required: false 14 | default: false 15 | type: boolean 16 | env: 17 | # PyPy is currently unsupported. 18 | CIBW_SKIP: pp* 19 | 20 | 21 | jobs: 22 | build_wheels: 23 | name: Build wheels for ${{ matrix.name }} 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: ${{ inputs.fail-fast }} 27 | matrix: 28 | include: 29 | # NOTE: This does compile on Linux but linux fails pytest 30 | - name: Windows 31 | os: windows-latest 32 | - name: Windows arm64 33 | os: windows-11-arm 34 | steps: 35 | - uses: actions/checkout@v6 36 | with: 37 | submodules: true 38 | - name: Enable CPython prerelease 39 | if: ${{ inputs.prerelease-pythons }} 40 | run: echo "CIBW_ENABLE=cpython-prerelease" >> $GITHUB_ENV 41 | - name: Build wheels 42 | uses: pypa/cibuildwheel@v3.3.0 43 | with: 44 | output-dir: dist 45 | 46 | - uses: actions/upload-artifact@v6 47 | with: 48 | name: wheels-${{ matrix.os }} 49 | path: dist/*.whl -------------------------------------------------------------------------------- /winloop/handles/stream.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVStream(UVBaseTransport): 2 | cdef: 3 | uv.uv_shutdown_t _shutdown_req 4 | bint __shutting_down 5 | bint __reading 6 | bint __read_error_close 7 | 8 | bint __buffered 9 | object _protocol_get_buffer 10 | object _protocol_buffer_updated 11 | 12 | bint _eof 13 | list _buffer 14 | size_t _buffer_size 15 | 16 | Py_buffer _read_pybuf 17 | bint _read_pybuf_acquired 18 | 19 | # All "inline" methods are final 20 | 21 | cdef inline _init(self, Loop loop, object protocol, Server server, 22 | object waiter, object context) 23 | 24 | 25 | cdef inline _shutdown(self) 26 | cdef inline _accept(self, UVStream server) 27 | 28 | cdef inline _close_on_read_error(self) 29 | 30 | cdef inline __reading_started(self) 31 | cdef inline __reading_stopped(self) 32 | 33 | # The user API write() and writelines() firstly call _buffer_write() to 34 | # buffer up user data chunks, potentially multiple times in writelines(), 35 | # and then call _initiate_write() to start writing either immediately or in 36 | # the next iteration (loop._queue_write()). 37 | cdef inline _buffer_write(self, object data) 38 | cdef inline _initiate_write(self) 39 | 40 | # _exec_write() is the method that does the actual send, and _try_write() 41 | # is a fast-path used in _exec_write() to send a single chunk. 42 | cdef inline _exec_write(self) 43 | cdef inline _try_write(self, object data) 44 | 45 | cdef _close(self) 46 | 47 | cdef inline _on_accept(self) 48 | cdef inline _on_eof(self) 49 | cdef inline _on_write(self) 50 | cdef inline _on_connect(self, object exc) 51 | -------------------------------------------------------------------------------- /winloop/handles/async_.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVAsync(UVHandle): 3 | cdef _init(self, Loop loop, method_t callback, object ctx): 4 | cdef int err 5 | 6 | self._start_init(loop) 7 | 8 | self._handle = PyMem_RawMalloc(sizeof(uv.uv_async_t)) 9 | if self._handle is NULL: 10 | self._abort_init() 11 | raise MemoryError() 12 | 13 | err = uv.uv_async_init(self._loop.uvloop, 14 | self._handle, 15 | __uvasync_callback) 16 | if err < 0: 17 | self._abort_init() 18 | raise convert_error(err) 19 | 20 | self._finish_init() 21 | 22 | self.callback = callback 23 | self.ctx = ctx 24 | 25 | cdef send(self): 26 | cdef int err 27 | 28 | self._ensure_alive() 29 | 30 | err = uv.uv_async_send(self._handle) 31 | if err < 0: 32 | exc = convert_error(err) 33 | self._fatal_error(exc, True) 34 | return 35 | 36 | @staticmethod 37 | cdef UVAsync new(Loop loop, method_t callback, object ctx): 38 | cdef UVAsync handle 39 | handle = UVAsync.__new__(UVAsync) 40 | handle._init(loop, callback, ctx) 41 | return handle 42 | 43 | 44 | cdef void __uvasync_callback( 45 | uv.uv_async_t* handle, 46 | ) noexcept with gil: 47 | if __ensure_handle_data(handle, "UVAsync callback") == 0: 48 | return 49 | 50 | cdef: 51 | UVAsync async_ = handle.data 52 | method_t cb = async_.callback 53 | try: 54 | cb(async_.ctx) 55 | except BaseException as ex: 56 | async_._error(ex, False) 57 | -------------------------------------------------------------------------------- /tests/test_aiomultiprocess.py: -------------------------------------------------------------------------------- 1 | try: 2 | import multiprocessing 3 | 4 | import aiomultiprocess 5 | except ImportError: 6 | skip_tests = True 7 | else: 8 | skip_tests = False 9 | import asyncio 10 | import random 11 | import unittest 12 | 13 | import winloop 14 | from winloop import _testbase as tb 15 | 16 | 17 | def my_map(): 18 | return [random.uniform(0.005, 1) for _ in range(7)] 19 | 20 | 21 | class _Test_Multiprocessing: 22 | """Used for Testing aiomultiprocessing""" 23 | 24 | @unittest.skip( 25 | "aiomultiprocess has an import bug releated to having a tests module" 26 | ) 27 | def test_process_spawning(self): 28 | # See: 29 | # https://github.com/Vizonex/Winloop/issues/11#issuecomment-1922659521 30 | self.processes = set() 31 | # There must be at least 2 pids setup for this to be correctly ran... 32 | total_processes = 2 33 | 34 | async def test(): 35 | async with aiomultiprocess.Pool(total_processes) as pool: 36 | self.processes.update(p.pid for p in pool.processes) 37 | await pool.map(asyncio.sleep, my_map()) 38 | await asyncio.sleep(0.005) 39 | self.assertEqual(len(self.processes), total_processes) 40 | 41 | self.loop.run_until_complete(test()) 42 | 43 | 44 | @unittest.skipIf(skip_tests, "no aiomultiprocess module") 45 | class Test_UV_AioMultiprocess(_Test_Multiprocessing, tb.UVTestCase): 46 | pass 47 | 48 | 49 | @unittest.skipIf(skip_tests, "no aiomultiprocess module") 50 | class Test_AIO_AioMultiprocess(_Test_Multiprocessing, tb.AIOTestCase): 51 | pass 52 | 53 | 54 | if __name__ == "__main__": 55 | # I have confirmed that multiprocessing is supported so no need 56 | # to continue with trying... - Vizonex... 57 | multiprocessing.freeze_support() 58 | # aiomultiprocess.set_start_method("spawn") 59 | winloop.install() 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /winloop/handles/handle.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | # NOTE: Uvloop expects you to use no_gc_clear 4 | # Reason beind doing so has to do with the debug 5 | # RuntimeError which hints at this and makes it 6 | # very clear to use it. 7 | # so please do not remove this wrapper, thank you :) 8 | @cython.no_gc_clear 9 | cdef class UVHandle: 10 | cdef: 11 | uv.uv_handle_t *_handle 12 | Loop _loop 13 | readonly _source_traceback 14 | bint _closed 15 | bint _inited 16 | object context 17 | 18 | # Added to enable current UDPTransport implementation, 19 | # which doesn't use libuv handles. 20 | bint _has_handle 21 | 22 | # All "inline" methods are final 23 | 24 | cdef inline int _start_init(self, Loop loop) except -1 25 | cdef inline int _abort_init(self) except -1 26 | cdef inline _finish_init(self) 27 | 28 | cdef inline bint _is_alive(self) except -1 29 | cdef inline int _ensure_alive(self) except -1 30 | 31 | cdef _error(self, exc, throw) 32 | # in CPython it returns NULL on exception raised 33 | # so let's define that an object of NONE is returning. 34 | cdef object _fatal_error(self, exc, throw, reason=?) 35 | cdef _warn_unclosed(self) 36 | 37 | cdef void _free(self) noexcept 38 | # TODO: Optimize to return an integer if 39 | # exception handling of CPython can be better learned. 40 | cdef _close(self) 41 | 42 | 43 | @cython.no_gc_clear 44 | cdef class UVSocketHandle(UVHandle): 45 | cdef: 46 | # Points to a Python file-object that should be closed 47 | # when the transport is closing. Used by pipes. This 48 | # should probably be refactored somehow. 49 | object _fileobj 50 | object __cached_socket 51 | 52 | # All "inline" methods are final 53 | 54 | cdef _fileno(self) 55 | 56 | cdef _new_socket(self) 57 | cdef inline _get_socket(self) 58 | cdef inline _attach_fileobj(self, object file) 59 | 60 | cdef _open(self, int sockfd) 61 | -------------------------------------------------------------------------------- /winloop/handles/idle.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVIdle(UVHandle): 3 | cdef _init(self, Loop loop, Handle h): 4 | cdef int err 5 | 6 | self._start_init(loop) 7 | 8 | self._handle = PyMem_RawMalloc(sizeof(uv.uv_idle_t)) 9 | if self._handle is NULL: 10 | self._abort_init() 11 | raise MemoryError() 12 | 13 | err = uv.uv_idle_init(self._loop.uvloop, self._handle) 14 | if err < 0: 15 | self._abort_init() 16 | raise convert_error(err) 17 | 18 | self._finish_init() 19 | 20 | self.h = h 21 | self.running = 0 22 | 23 | cdef inline stop(self): 24 | cdef int err 25 | 26 | if not self._is_alive(): 27 | self.running = 0 28 | return 29 | 30 | if self.running == 1: 31 | err = uv.uv_idle_stop(self._handle) 32 | self.running = 0 33 | if err < 0: 34 | exc = convert_error(err) 35 | self._fatal_error(exc, True) 36 | return 37 | 38 | cdef inline start(self): 39 | cdef int err 40 | 41 | self._ensure_alive() 42 | 43 | if self.running == 0: 44 | err = uv.uv_idle_start(self._handle, 45 | cb_idle_callback) 46 | if err < 0: 47 | exc = convert_error(err) 48 | self._fatal_error(exc, True) 49 | return 50 | self.running = 1 51 | 52 | @staticmethod 53 | cdef UVIdle new(Loop loop, Handle h): 54 | cdef UVIdle handle 55 | handle = UVIdle.__new__(UVIdle) 56 | handle._init(loop, h) 57 | return handle 58 | 59 | 60 | cdef void cb_idle_callback( 61 | uv.uv_idle_t* handle, 62 | ) noexcept with gil: 63 | if __ensure_handle_data(handle, "UVIdle callback") == 0: 64 | return 65 | 66 | cdef: 67 | UVIdle idle = handle.data 68 | Handle h = idle.h 69 | try: 70 | h._run() 71 | except BaseException as ex: 72 | idle._error(ex, False) 73 | -------------------------------------------------------------------------------- /winloop/handles/check.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVCheck(UVHandle): 3 | cdef _init(self, Loop loop, Handle h): 4 | cdef int err 5 | 6 | self._start_init(loop) 7 | 8 | self._handle = PyMem_RawMalloc(sizeof(uv.uv_check_t)) 9 | if self._handle is NULL: 10 | self._abort_init() 11 | raise MemoryError() 12 | 13 | err = uv.uv_check_init(self._loop.uvloop, self._handle) 14 | if err < 0: 15 | self._abort_init() 16 | raise convert_error(err) 17 | 18 | self._finish_init() 19 | 20 | self.h = h 21 | self.running = 0 22 | 23 | cdef inline stop(self): 24 | cdef int err 25 | 26 | if not self._is_alive(): 27 | self.running = 0 28 | return 29 | 30 | if self.running == 1: 31 | err = uv.uv_check_stop(self._handle) 32 | self.running = 0 33 | if err < 0: 34 | exc = convert_error(err) 35 | self._fatal_error(exc, True) 36 | return 37 | 38 | cdef inline start(self): 39 | cdef int err 40 | 41 | self._ensure_alive() 42 | 43 | if self.running == 0: 44 | err = uv.uv_check_start(self._handle, 45 | cb_check_callback) 46 | if err < 0: 47 | exc = convert_error(err) 48 | self._fatal_error(exc, True) 49 | return 50 | self.running = 1 51 | 52 | @staticmethod 53 | cdef UVCheck new(Loop loop, Handle h): 54 | cdef UVCheck handle 55 | handle = UVCheck.__new__(UVCheck) 56 | handle._init(loop, h) 57 | return handle 58 | 59 | 60 | cdef void cb_check_callback( 61 | uv.uv_check_t* handle, 62 | ) noexcept with gil: 63 | if __ensure_handle_data(handle, "UVCheck callback") == 0: 64 | return 65 | 66 | cdef: 67 | UVCheck check = handle.data 68 | Handle h = check.h 69 | try: 70 | h._run() 71 | except BaseException as ex: 72 | check._error(ex, False) 73 | -------------------------------------------------------------------------------- /tests/test_dealloc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import subprocess 3 | import sys 4 | 5 | from winloop import _testbase as tb 6 | 7 | 8 | class TestDealloc(tb.UVTestCase): 9 | def test_dealloc_1(self): 10 | # Somewhere between Cython 0.25.2 and 0.26.0 uvloop programs 11 | # started to trigger the following output: 12 | # 13 | # $ python prog.py 14 | # Error in sys.excepthook: 15 | # 16 | # Original exception was: 17 | # 18 | # Upon some debugging, it appeared that Handle.__dealloc__ was 19 | # called at a time where some CPython objects become non-functional, 20 | # and any exception in __dealloc__ caused CPython to output the 21 | # above. 22 | # 23 | # This regression test starts an event loop in debug mode, 24 | # lets it run for a brief period of time, and exits the program. 25 | # This will trigger Handle.__dealloc__, CallbackHandle.__dealloc__, 26 | # and Loop.__dealloc__ methods. The test will fail if they produce 27 | # any unwanted output. 28 | 29 | async def test(): 30 | prog = """\ 31 | import winloop as uvloop 32 | 33 | async def foo(): 34 | return 42 35 | 36 | def main(): 37 | loop = uvloop.new_event_loop() 38 | loop.set_debug(True) 39 | loop.run_until_complete(foo()) 40 | # Do not close the loop on purpose: let __dealloc__ methods run. 41 | 42 | if __name__ == '__main__': 43 | main() 44 | """ 45 | 46 | cmd = sys.executable 47 | proc = await asyncio.create_subprocess_exec( 48 | cmd, 49 | b"-W", 50 | b"ignore", 51 | b"-c", 52 | prog, 53 | stdout=subprocess.PIPE, 54 | stderr=subprocess.PIPE, 55 | ) 56 | 57 | await proc.wait() 58 | out = await proc.stdout.read() 59 | err = await proc.stderr.read() 60 | 61 | return out, err 62 | 63 | out, err = self.loop.run_until_complete(test()) 64 | self.assertEqual(out, b"", "stdout is not empty") 65 | self.assertEqual(err, b"", "stderr is not empty") 66 | -------------------------------------------------------------------------------- /tests/certs/ssl_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF8TCCBFmgAwIBAgIJAMstgJlaaVJcMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNV 3 | BAYTAlhZMSYwJAYDVQQKDB1QeXRob24gU29mdHdhcmUgRm91bmRhdGlvbiBDQTEW 4 | MBQGA1UEAwwNb3VyLWNhLXNlcnZlcjAeFw0xODA4MjkxNDIzMTZaFw0yODA3MDcx 5 | NDIzMTZaMF8xCzAJBgNVBAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEj 6 | MCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRpb24xEjAQBgNVBAMMCWxv 7 | Y2FsaG9zdDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJ8oLzdB739k 8 | YxZiFukBFGIpyjqYkj0I015p/sDz1MT7DljcZLBLy7OqnkLpB5tnM8256DwdihPA 9 | 3zlnfEzTfr9DD0qFBW2H5cMCoz7X17koeRhzGDd3dkjUeBjXvR5qRosG8wM3lQug 10 | U7AizY+3Azaj1yN3mZ9K5a20jr58Kqinz+Xxx6sb2JfYYff2neJbBahNm5id0AD2 11 | pi/TthZqO5DURJYo+MdgZOcy+7jEjOJsLWZd3Yzq78iM07qDjbpIoVpENZCTHTWA 12 | hX8LIqz0OBmh4weQpm4+plU7E4r4D82uauocWw8iyuznCTtABWO7n9fWySmf9QZC 13 | WYxHAFpBQs6zUVqAD7nhFdTqpQ9bRiaEnjE4HiAccPW+MAoSxFnv/rNzEzI6b4zU 14 | NspFMfg1aNVamdjxdpUZ1GG1Okf0yPJykqEX4PZl3La1Be2q7YZ1wydR523Xd+f3 15 | EO4/g+imETSKn8gyCf6Rvib175L4r2WV1CXQH7gFwZYCod6WHYq5TQIDAQABo4IB 16 | wDCCAbwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA4GA1UdDwEB/wQEAwIFoDAdBgNV 17 | HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E 18 | FgQUj+od4zNcABazi29rb9NMy7XLfFUwfQYDVR0jBHYwdIAU3b/K2ubRNLo3dSHK 19 | b5oIKPI1tkihUaRPME0xCzAJBgNVBAYTAlhZMSYwJAYDVQQKDB1QeXRob24gU29m 20 | dHdhcmUgRm91bmRhdGlvbiBDQTEWMBQGA1UEAwwNb3VyLWNhLXNlcnZlcoIJAMst 21 | gJlaaVJbMIGDBggrBgEFBQcBAQR3MHUwPAYIKwYBBQUHMAKGMGh0dHA6Ly90ZXN0 22 | Y2EucHl0aG9udGVzdC5uZXQvdGVzdGNhL3B5Y2FjZXJ0LmNlcjA1BggrBgEFBQcw 23 | AYYpaHR0cDovL3Rlc3RjYS5weXRob250ZXN0Lm5ldC90ZXN0Y2Evb2NzcC8wQwYD 24 | VR0fBDwwOjA4oDagNIYyaHR0cDovL3Rlc3RjYS5weXRob250ZXN0Lm5ldC90ZXN0 25 | Y2EvcmV2b2NhdGlvbi5jcmwwDQYJKoZIhvcNAQELBQADggGBACf1jFkQ9MbnKAC/ 26 | uo17EwPxHKZfswZVpCK527LVRr33DN1DbrR5ZWchDCpV7kCOhZ+fR7sKKk22ZHSY 27 | oH+u3PEu20J3GOB1iyY1aMNB7WvId3JvappdVWkC/VpUyFfLsGUDFuIPADmZZqCb 28 | iJMX4loteTVfl1d4xK/1mV6Gq9MRrRqiDfpSELn+v53OM9mGspwW+NZ1CIrbCuW0 29 | KxZ/tPkqn8PSd9fNZR70bB7rWbnwrl+kH8xKxLl6qdlrMmg74WWwhLeQxK7+9DdP 30 | IaDenzqx5cwWBGY/C0HcQj0gPuy3lSs1V/q+f7Y6uspPWP51PgiJLIywXS75iRAr 31 | +UFGTzwAtyfTZSQoFyMmMULqfk6T5HtoVMqfRvPvK+mFDLWEstU1NIB1K/CRI7gI 32 | AY65ClTU+zRS/tlF8IA7tsFvgtEf8jsI9kamlidhS1gyeg4dWcVErV4aeTPB1AUv 33 | StPYQkKNM+NjytWHl5tNuBoDNLsc0gI/WSPiI4CIY8LwomOoiw== 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /tests/test_sourcecode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import unittest 5 | 6 | 7 | def find_uvloop_root(): 8 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | 11 | class TestSourceCode(unittest.TestCase): 12 | def test_flake8(self): 13 | edgepath = find_uvloop_root() 14 | config_path = os.path.join(edgepath, ".flake8") 15 | if not os.path.exists(config_path): 16 | raise RuntimeError("could not locate .flake8 file") 17 | 18 | try: 19 | import flake8 # NoQA 20 | except ImportError: 21 | raise unittest.SkipTest("flake8 module is missing") 22 | 23 | for subdir in ["examples", "winloop", "tests"]: 24 | try: 25 | subprocess.run( 26 | [sys.executable, "-m", "flake8", "--config", config_path], 27 | check=True, 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.PIPE, 30 | cwd=os.path.join(edgepath, subdir), 31 | ) 32 | except subprocess.CalledProcessError as ex: 33 | output = ex.stdout.decode() 34 | output += "\n" 35 | output += ex.stderr.decode() 36 | raise AssertionError( 37 | "flake8 validation failed: {}\n{}".format(ex, output) 38 | ) from None 39 | 40 | def test_mypy(self): 41 | edgepath = find_uvloop_root() 42 | config_path = os.path.join(edgepath, "mypy.ini") 43 | if not os.path.exists(config_path): 44 | raise RuntimeError("could not locate mypy.ini file") 45 | 46 | try: 47 | import mypy # NoQA 48 | except ImportError: 49 | raise unittest.SkipTest("mypy module is missing") 50 | 51 | try: 52 | subprocess.run( 53 | [sys.executable, "-m", "mypy", "--config-file", config_path, "winloop"], 54 | check=True, 55 | stdout=subprocess.PIPE, 56 | stderr=subprocess.PIPE, 57 | cwd=edgepath, 58 | ) 59 | except subprocess.CalledProcessError as ex: 60 | output = ex.stdout.decode() 61 | output += "\n" 62 | output += ex.stderr.decode() 63 | raise AssertionError( 64 | "mypy validation failed: {}\n{}".format(ex, output) 65 | ) from None 66 | -------------------------------------------------------------------------------- /winloop/includes/system.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport int8_t, uint64_t 2 | 3 | 4 | cdef extern from "includes/compat.h" nogil: 5 | 6 | int ntohl(int) 7 | int htonl(int) 8 | int ntohs(int) 9 | 10 | struct sockaddr: 11 | unsigned short sa_family 12 | char sa_data[14] 13 | 14 | struct addrinfo: 15 | int ai_flags 16 | int ai_family 17 | int ai_socktype 18 | int ai_protocol 19 | size_t ai_addrlen 20 | sockaddr* ai_addr 21 | char* ai_canonname 22 | addrinfo* ai_next 23 | 24 | struct sockaddr_in: 25 | unsigned short sin_family 26 | unsigned short sin_port 27 | # ... 28 | 29 | struct sockaddr_in6: 30 | unsigned short sin6_family 31 | unsigned short sin6_port 32 | unsigned long sin6_flowinfo 33 | # ... 34 | unsigned long sin6_scope_id 35 | 36 | struct sockaddr_storage: 37 | unsigned short ss_family 38 | # ... 39 | 40 | const char *gai_strerror(int errcode) 41 | 42 | int socketpair(int domain, int type, int protocol, int socket_vector[2]) 43 | 44 | int setsockopt(int socket, int level, int option_name, 45 | const void *option_value, int option_len) 46 | 47 | struct sockaddr_un: 48 | unsigned short sun_family 49 | char* sun_path 50 | # ... 51 | 52 | ssize_t write(int fd, const void *buf, size_t count) 53 | void _exit(int status) 54 | 55 | cdef int EWOULDBLOCK 56 | 57 | cdef int PLATFORM_IS_APPLE 58 | cdef int PLATFORM_IS_LINUX 59 | cdef int PLATFORM_IS_WINDOWS 60 | 61 | struct epoll_event: 62 | # We don't use the fields 63 | pass 64 | 65 | int EPOLL_CTL_DEL 66 | int epoll_ctl(int epfd, int op, int fd, epoll_event *event) 67 | object MakeUnixSockPyAddr(sockaddr_un *addr) 68 | 69 | 70 | cdef extern from "includes/fork_handler.h": 71 | 72 | uint64_t MAIN_THREAD_ID 73 | int8_t MAIN_THREAD_ID_SET 74 | ctypedef void (*OnForkHandler)() 75 | void handleAtFork() 76 | void setForkHandler(OnForkHandler handler) 77 | void resetForkHandler() 78 | void setMainThreadID(uint64_t id) 79 | 80 | int pthread_atfork( 81 | void (*prepare)(), 82 | void (*parent)(), 83 | void (*child)()) 84 | -------------------------------------------------------------------------------- /winloop/request.pyx: -------------------------------------------------------------------------------- 1 | cdef class UVRequest: 2 | """A base class for all libuv requests (uv_getaddrinfo_t, etc). 3 | 4 | Important: it's a responsibility of the subclass to call the 5 | "on_done" method in the request's callback. 6 | 7 | If "on_done" isn't called, the request object will never die. 8 | """ 9 | 10 | def __cinit__(self, Loop loop, *_): 11 | self.request = NULL 12 | self.loop = loop 13 | self.done = 0 14 | Py_INCREF(self) 15 | 16 | cdef on_done(self): 17 | self.done = 1 18 | Py_DECREF(self) 19 | 20 | cdef cancel(self): 21 | # Most requests are implemented using a threadpool. It's only 22 | # possible to cancel a request when it's still in a threadpool's 23 | # queue. Once it's started to execute, we have to wait until 24 | # it finishes and calls its callback (and callback *must* call 25 | # UVRequest.on_done). 26 | 27 | cdef int err 28 | 29 | if self.done == 1: 30 | return 31 | 32 | if UVLOOP_DEBUG: 33 | if self.request is NULL: 34 | raise RuntimeError( 35 | '{}.cancel: .request is NULL'.format( 36 | self.__class__.__name__)) 37 | 38 | if self.request.data is NULL: 39 | raise RuntimeError( 40 | '{}.cancel: .request.data is NULL'.format( 41 | self.__class__.__name__)) 42 | 43 | if self.request.data is not self: 44 | raise RuntimeError( 45 | '{}.cancel: .request.data is not UVRequest'.format( 46 | self.__class__.__name__)) 47 | 48 | # We only can cancel pending requests. Let's try. 49 | err = uv.uv_cancel(self.request) 50 | if err < 0: 51 | if err == uv.UV_EBUSY: 52 | # Can't close the request -- it's executing (see the first 53 | # comment). Loop will have to wait until the callback 54 | # fires. 55 | pass 56 | elif err == uv.UV_EINVAL: 57 | # From libuv docs: 58 | # 59 | # Only cancellation of uv_fs_t, uv_getaddrinfo_t, 60 | # uv_getnameinfo_t and uv_work_t requests is currently 61 | # supported. 62 | return 63 | else: 64 | ex = convert_error(err) 65 | self.loop._handle_exception(ex) 66 | -------------------------------------------------------------------------------- /tests/certs/ssl_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCfKC83Qe9/ZGMW 3 | YhbpARRiKco6mJI9CNNeaf7A89TE+w5Y3GSwS8uzqp5C6QebZzPNueg8HYoTwN85 4 | Z3xM036/Qw9KhQVth+XDAqM+19e5KHkYcxg3d3ZI1HgY170eakaLBvMDN5ULoFOw 5 | Is2PtwM2o9cjd5mfSuWttI6+fCqop8/l8cerG9iX2GH39p3iWwWoTZuYndAA9qYv 6 | 07YWajuQ1ESWKPjHYGTnMvu4xIzibC1mXd2M6u/IjNO6g426SKFaRDWQkx01gIV/ 7 | CyKs9DgZoeMHkKZuPqZVOxOK+A/NrmrqHFsPIsrs5wk7QAVju5/X1skpn/UGQlmM 8 | RwBaQULOs1FagA+54RXU6qUPW0YmhJ4xOB4gHHD1vjAKEsRZ7/6zcxMyOm+M1DbK 9 | RTH4NWjVWpnY8XaVGdRhtTpH9MjycpKhF+D2Zdy2tQXtqu2GdcMnUedt13fn9xDu 10 | P4PophE0ip/IMgn+kb4m9e+S+K9lldQl0B+4BcGWAqHelh2KuU0CAwEAAQKCAYEA 11 | lKiWIYjmyRjdLKUGPTES9vWNvNmRjozV0RQ0LcoSbMMLDZkeO0UwyWqOVHUQ8+ib 12 | jIcfEjeNJxI57oZopeHOO5vJhpNlFH+g7ltiW2qERqA1K88lSXm99Bzw6FNqhCRE 13 | K8ub5N9fyfJA+P4o/xm0WK8EXk5yIUV17p/9zJJxzgKgv2jsVTi3QG2OZGvn4Oug 14 | ByomMZEGHkBDzdxz8c/cP1Tlk1RFuwSgews178k2xq7AYSM/s0YmHi7b/RSvptX6 15 | 1v8P8kXNUe4AwTaNyrlvF2lwIadZ8h1hA7tCE2n44b7a7KfhAkwcbr1T59ioYh6P 16 | zxsyPT678uD51dbtD/DXJCcoeeFOb8uzkR2KNcrnQzZpCJnRq4Gp5ybxwsxxuzpr 17 | gz0gbNlhuWtE7EoSzmIK9t+WTS7IM2CvZymd6/OAh1Fuw6AQhSp64XRp3OfMMAAC 18 | Ie2EPtKj4islWGT8VoUjuRYGmdRh4duAH1dkiAXOWA3R7y5a1/y/iE8KE8BtxocB 19 | AoHBAM8aiURgpu1Fs0Oqz6izec7KSLL3l8hmW+MKUOfk/Ybng6FrTFsL5YtzR+Ap 20 | wW4wwWnnIKEc1JLiZ7g8agRETK8hr5PwFXUn/GSWC0SMsazLJToySQS5LOV0tLzK 21 | kJ3jtNU7tnlDGNkCHTHSoVL2T/8t+IkZI/h5Z6wjlYPvU2Iu0nVIXtiG+alv4A6M 22 | Hrh9l5or4mjB6rGnVXeYohLkCm6s/W97ahVxLMcEdbsBo1prm2JqGnSoiR/tEFC/ 23 | QHQnbQKBwQDEu7kW0Yg9sZ89QtYtVQ1YpixFZORaUeRIRLnpEs1w7L1mCbOZ2Lj9 24 | JHxsH05cYAc7HJfPwwxv3+3aGAIC/dfu4VSwEFtatAzUpzlhzKS5+HQCWB4JUNNU 25 | MQ3+FwK2xQX4Ph8t+OzrFiYcK2g0An5UxWMa2HWIAWUOhnTOydAVsoH6yP31cVm4 26 | 0hxoABCwflaNLNGjRUyfBpLTAcNu/YtcE+KREy7YAAgXXrhRSO4XpLsSXwLnLT7/ 27 | YOkoBWDcTWECgcBPWnSUDZCIQ3efithMZJBciqd2Y2X19Dpq8O31HImD4jtOY0V7 28 | cUB/wSkeHAGwjd/eCyA2e0x8B2IEdqmMfvr+86JJxekC3dJYXCFvH5WIhsH53YCa 29 | 3bT1KlWCLP9ib/g+58VQC0R/Cc9T4sfLePNH7D5ZkZd1wlbV30CPr+i8KwKay6MD 30 | xhvtLx+jk07GE+E9wmjbCMo7TclyrLoVEOlqZMAqshgApT+p9eyCPetwXuDHwa3n 31 | WxhHclcZCV7R4rUCgcAkdGSnxcvpIrDPOUNWwxvmAWTStw9ZbTNP8OxCNCm9cyDl 32 | d4bAS1h8D/a+Uk7C70hnu7Sl2w7C7Eu2zhwRUdhhe3+l4GINPK/j99i6NqGPlGpq 33 | xMlMEJ4YS768BqeKFpg0l85PRoEgTsphDeoROSUPsEPdBZ9BxIBlYKTkbKESZDGR 34 | twzYHljx1n1NCDYPflmrb1KpXn4EOcObNghw2KqqNUUWfOeBPwBA1FxzM4BrAStp 35 | DBINpGS4Dc0mjViVegECgcA3hTtm82XdxQXj9LQmb/E3lKx/7H87XIOeNMmvjYuZ 36 | iS9wKrkF+u42vyoDxcKMCnxP5056wpdST4p56r+SBwVTHcc3lGBSGcMTIfwRXrj3 37 | thOA2our2n4ouNIsYyTlcsQSzifwmpRmVMRPxl9fYVdEWUgB83FgHT0D9avvZnF9 38 | t9OccnGJXShAIZIBADhVj/JwG4FbaX42NijD5PNpVLk1Y17OV0I576T9SfaQoBjJ 39 | aH1M/zC4aVaS0DYB/Gxq7v8= 40 | -----END PRIVATE KEY----- 41 | -------------------------------------------------------------------------------- /winloop/handles/process.pxd: -------------------------------------------------------------------------------- 1 | cdef class UVProcess(UVHandle): 2 | cdef: 3 | object _returncode 4 | object _pid 5 | 6 | object _errpipe_read 7 | object _errpipe_write 8 | object _preexec_fn 9 | bint _restore_signals 10 | 11 | list _fds_to_close 12 | 13 | # Attributes used to compose uv_process_options_t: 14 | uv.uv_process_options_t options 15 | uv.uv_stdio_container_t[3] iocnt 16 | list __env 17 | char **uv_opt_env 18 | list __args 19 | char **uv_opt_args 20 | char *uv_opt_file 21 | bytes __cwd 22 | 23 | cdef _close_process_handle(self) 24 | 25 | cdef _init(self, Loop loop, list args, dict env, cwd, 26 | start_new_session, 27 | _stdin, _stdout, _stderr, pass_fds, 28 | debug_flags, preexec_fn, restore_signals) 29 | 30 | cdef _after_fork(self) 31 | 32 | cdef char** __to_cstring_array(self, list arr) 33 | cdef _init_args(self, list args) 34 | cdef _init_env(self, dict env) 35 | cdef _init_files(self, _stdin, _stdout, _stderr) 36 | cdef _init_options(self, list args, dict env, cwd, start_new_session, 37 | _stdin, _stdout, _stderr, bint force_fork) 38 | 39 | cdef _close_after_spawn(self, int fd) 40 | 41 | cdef _on_exit(self, int64_t exit_status, int term_signal) 42 | cdef _kill(self, int signum) 43 | 44 | 45 | cdef class UVProcessTransport(UVProcess): 46 | cdef: 47 | list _exit_waiters 48 | list _init_futs 49 | bint _stdio_ready 50 | list _pending_calls 51 | object _protocol 52 | bint _finished 53 | 54 | WriteUnixTransport _stdin 55 | ReadUnixTransport _stdout 56 | ReadUnixTransport _stderr 57 | 58 | object stdin_proto 59 | object stdout_proto 60 | object stderr_proto 61 | 62 | cdef _file_redirect_stdio(self, int fd) 63 | cdef _file_devnull(self) 64 | cdef _file_inpipe(self) 65 | cdef _file_outpipe(self) 66 | 67 | cdef _check_proc(self) 68 | cdef _pipe_connection_lost(self, int fd, exc) 69 | cdef _pipe_data_received(self, int fd, data) 70 | 71 | cdef _call_connection_made(self, waiter) 72 | cdef _try_finish(self) 73 | 74 | @staticmethod 75 | cdef UVProcessTransport new(Loop loop, protocol, args, env, cwd, 76 | start_new_session, 77 | _stdin, _stdout, _stderr, pass_fds, 78 | waiter, 79 | debug_flags, 80 | preexec_fn, restore_signals) 81 | -------------------------------------------------------------------------------- /examples/bench/rlserver.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import gc 4 | import os.path 5 | import socket as stdsock 6 | 7 | 8 | PRINT = 0 9 | 10 | 11 | async def echo_client_streams(reader, writer): 12 | sock = writer.get_extra_info("socket") 13 | try: 14 | sock.setsockopt(stdsock.IPPROTO_TCP, stdsock.TCP_NODELAY, 1) 15 | except (OSError, NameError): 16 | pass 17 | if PRINT: 18 | print("Connection from", sock.getpeername()) 19 | while True: 20 | data = await reader.readline() 21 | if not data: 22 | break 23 | writer.write(data) 24 | if PRINT: 25 | print("Connection closed") 26 | writer.close() 27 | 28 | 29 | async def print_debug(loop): 30 | while True: 31 | print(chr(27) + "[2J") # clear screen 32 | loop.print_debug_info() 33 | await asyncio.sleep(0.5) 34 | 35 | 36 | if __name__ == "__main__": 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument("--uvloop", default=False, action="store_true") 39 | parser.add_argument("--addr", default="127.0.0.1:25000", type=str) 40 | parser.add_argument("--print", default=False, action="store_true") 41 | args = parser.parse_args() 42 | 43 | if args.uvloop: 44 | import winloop as uvloop 45 | 46 | loop = uvloop.new_event_loop() 47 | print("using UVLoop") 48 | else: 49 | loop = asyncio.new_event_loop() 50 | print("using asyncio loop") 51 | 52 | asyncio.set_event_loop(loop) 53 | loop.set_debug(False) 54 | 55 | if args.print: 56 | PRINT = 1 57 | 58 | if hasattr(loop, "print_debug_info"): 59 | loop.create_task(print_debug(loop)) 60 | PRINT = 0 61 | 62 | unix = False 63 | if args.addr.startswith("file:"): 64 | unix = True 65 | addr = args.addr[5:] 66 | if os.path.exists(addr): 67 | os.remove(addr) 68 | else: 69 | addr = args.addr.split(":") 70 | addr[1] = int(addr[1]) 71 | addr = tuple(addr) 72 | 73 | print("readline performance test") 74 | print("serving on: {}".format(addr)) 75 | 76 | print("using asyncio/streams") 77 | if unix: 78 | coro = asyncio.start_unix_server(echo_client_streams, addr, limit=256000) 79 | else: 80 | coro = asyncio.start_server(echo_client_streams, *addr, limit=256000) 81 | srv = loop.run_until_complete(coro) 82 | 83 | try: 84 | loop.run_forever() 85 | finally: 86 | if hasattr(loop, "print_debug_info"): 87 | gc.collect() 88 | print(chr(27) + "[2J") 89 | loop.print_debug_info() 90 | 91 | loop.close() 92 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "winloop" 3 | description = "Windows version of uvloop" 4 | authors = [{name = "Vizonex", email="VizonexBusiness@gmail.com"}] 5 | requires-python = '>=3.9.0' 6 | readme = "README.md" 7 | license = {text = "MIT License"} 8 | dynamic = ["version"] 9 | keywords = [ 10 | "asyncio", 11 | "networking", 12 | ] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: AsyncIO", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: Apache Software License", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: POSIX", 20 | "Operating System :: MacOS :: MacOS X", 21 | "Operating System :: Microsoft :: Windows", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: 3.13", 29 | "Programming Language :: Python :: Implementation :: CPython", 30 | "Topic :: System :: Networking", 31 | ] 32 | 33 | [project.urls] 34 | github = "https://github.com/Vizonex/Winloop" 35 | 36 | # TODO: We really need to get dependabot in a future update moving versions ourselves is annoying... 37 | 38 | [project.optional-dependencies] 39 | test = [ 40 | # pycodestyle is a dependency of flake8, but it must be frozen because 41 | # their combination breaks too often 42 | # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) 43 | 'aiohttp>=3.10.5', 44 | 'flake8>=5,<8', 45 | 'psutil', 46 | 'pycodestyle>=2.9,<2.15', 47 | 'pyOpenSSL>=23.0,<25.4', 48 | 'mypy>=0.800' 49 | ] 50 | dev = [ 51 | 'setuptools>=60', 52 | 'Cython==3.2.3', 53 | ] 54 | docs = [ 55 | 'Sphinx>=4.1.2,<7.5.0', 56 | 'sphinxcontrib-asyncio~=0.3.0', 57 | 'sphinx_rtd_theme>=0.5.2,<3.1.0', 58 | ] 59 | 60 | [build-system] 61 | requires = [ 62 | "setuptools>=60", 63 | "wheel", 64 | "Cython==3.2.3", 65 | ] 66 | build-backend = "setuptools.build_meta" 67 | 68 | [tool.setuptools] 69 | zip-safe = false 70 | packages = ["winloop"] 71 | 72 | [tool.setuptools.exclude-package-data] 73 | "*" = ["*.c", "*.h"] 74 | 75 | [tool.cibuildwheel] 76 | build-frontend = "build" 77 | #test-extras = "test" 78 | #test-command = "python -m unittest discover -v {project}/tests" 79 | 80 | [tool.pytest.ini_options] 81 | addopts = "--capture=no --assert=plain --strict-markers --tb=native --import-mode=importlib" 82 | testpaths = "tests" 83 | filterwarnings = "default" 84 | 85 | [tool.ruff.lint] 86 | # Upgrade annotations 87 | select = ["UP045", "UP007", "UP035"] 88 | -------------------------------------------------------------------------------- /winloop/handles/timer.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVTimer(UVHandle): 3 | cdef _init(self, Loop loop, method_t callback, object ctx, 4 | uint64_t timeout): 5 | 6 | cdef int err 7 | 8 | self._start_init(loop) 9 | 10 | self._handle = PyMem_RawMalloc(sizeof(uv.uv_timer_t)) 11 | if self._handle is NULL: 12 | self._abort_init() 13 | raise MemoryError() 14 | 15 | err = uv.uv_timer_init(self._loop.uvloop, self._handle) 16 | if err < 0: 17 | self._abort_init() 18 | raise convert_error(err) 19 | 20 | self._finish_init() 21 | 22 | self.callback = callback 23 | self.ctx = ctx 24 | self.running = 0 25 | self.timeout = timeout 26 | self.start_t = 0 27 | 28 | cdef stop(self): 29 | cdef int err 30 | 31 | if not self._is_alive(): 32 | self.running = 0 33 | return 34 | 35 | if self.running == 1: 36 | err = uv.uv_timer_stop(self._handle) 37 | self.running = 0 38 | if err < 0: 39 | exc = convert_error(err) 40 | self._fatal_error(exc, True) 41 | return 42 | 43 | cdef start(self): 44 | cdef int err 45 | 46 | self._ensure_alive() 47 | 48 | if self.running == 0: 49 | # Update libuv internal time. 50 | uv.uv_update_time(self._loop.uvloop) # void 51 | self.start_t = uv.uv_now(self._loop.uvloop) 52 | 53 | err = uv.uv_timer_start(self._handle, 54 | __uvtimer_callback, 55 | self.timeout, 0) 56 | if err < 0: 57 | exc = convert_error(err) 58 | self._fatal_error(exc, True) 59 | return 60 | self.running = 1 61 | 62 | cdef get_when(self): 63 | return self.start_t + self.timeout 64 | 65 | @staticmethod 66 | cdef UVTimer new(Loop loop, method_t callback, object ctx, 67 | uint64_t timeout): 68 | 69 | cdef UVTimer handle 70 | handle = UVTimer.__new__(UVTimer) 71 | handle._init(loop, callback, ctx, timeout) 72 | return handle 73 | 74 | 75 | cdef void __uvtimer_callback( 76 | uv.uv_timer_t* handle, 77 | ) noexcept with gil: 78 | if __ensure_handle_data(handle, "UVTimer callback") == 0: 79 | return 80 | 81 | cdef: 82 | UVTimer timer = handle.data 83 | method_t cb = timer.callback 84 | 85 | timer.running = 0 86 | try: 87 | cb(timer.ctx) 88 | except BaseException as ex: 89 | timer._error(ex, False) 90 | -------------------------------------------------------------------------------- /winloop/handles/fsevent.pyx: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class FileSystemEvent(enum.IntEnum): 5 | RENAME = uv.UV_RENAME 6 | CHANGE = uv.UV_CHANGE 7 | RENAME_CHANGE = RENAME | CHANGE 8 | 9 | 10 | @cython.no_gc_clear 11 | cdef class UVFSEvent(UVHandle): 12 | cdef _init(self, Loop loop, object callback, object context): 13 | cdef int err 14 | 15 | self._start_init(loop) 16 | 17 | self._handle = PyMem_RawMalloc( 18 | sizeof(uv.uv_fs_event_t) 19 | ) 20 | if self._handle is NULL: 21 | self._abort_init() 22 | raise MemoryError() 23 | 24 | err = uv.uv_fs_event_init( 25 | self._loop.uvloop, self._handle 26 | ) 27 | if err < 0: 28 | self._abort_init() 29 | raise convert_error(err) 30 | 31 | self._finish_init() 32 | 33 | self.running = 0 34 | self.callback = callback 35 | if context is None: 36 | context = Context_CopyCurrent() 37 | self.context = context 38 | 39 | cdef start(self, char* path, int flags): 40 | cdef int err 41 | 42 | self._ensure_alive() 43 | 44 | if self.running == 0: 45 | err = uv.uv_fs_event_start( 46 | self._handle, 47 | __uvfsevent_callback, 48 | path, 49 | flags, 50 | ) 51 | if err < 0: 52 | exc = convert_error(err) 53 | self._fatal_error(exc, True) 54 | return 55 | self.running = 1 56 | 57 | cdef stop(self): 58 | cdef int err 59 | 60 | if not self._is_alive(): 61 | self.running = 0 62 | return 63 | 64 | if self.running == 1: 65 | err = uv.uv_fs_event_stop(self._handle) 66 | self.running = 0 67 | if err < 0: 68 | exc = convert_error(err) 69 | self._fatal_error(exc, True) 70 | return 71 | 72 | cdef _close(self): 73 | try: 74 | self.stop() 75 | finally: 76 | UVHandle._close(self) 77 | 78 | def cancel(self): 79 | self._close() 80 | 81 | def cancelled(self): 82 | return self.running == 0 83 | 84 | @staticmethod 85 | cdef UVFSEvent new(Loop loop, object callback, object context): 86 | cdef UVFSEvent handle 87 | handle = UVFSEvent.__new__(UVFSEvent) 88 | handle._init(loop, callback, context) 89 | return handle 90 | 91 | 92 | cdef void __uvfsevent_callback( 93 | uv.uv_fs_event_t* handle, 94 | const char *filename, 95 | int events, 96 | int status, 97 | ) noexcept with gil: 98 | if __ensure_handle_data( 99 | handle, "UVFSEvent callback" 100 | ) == 0: 101 | return 102 | 103 | cdef: 104 | UVFSEvent fs_event = handle.data 105 | Handle h 106 | 107 | try: 108 | h = new_Handle( 109 | fs_event._loop, 110 | fs_event.callback, 111 | (filename, FileSystemEvent(events)), 112 | fs_event.context, 113 | ) 114 | h._run() 115 | except BaseException as ex: 116 | fs_event._error(ex, False) 117 | -------------------------------------------------------------------------------- /examples/bench/echoclient.py: -------------------------------------------------------------------------------- 1 | # Copied with minimal modifications from curio 2 | # https://github.com/dabeaz/curio 3 | 4 | 5 | import argparse 6 | import concurrent.futures 7 | import socket 8 | import ssl 9 | import time 10 | 11 | 12 | if __name__ == "__main__": 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("--msize", default=1000, type=int, help="message size in bytes") 15 | parser.add_argument("--mpr", default=1, type=int, help="messages per request") 16 | parser.add_argument("--num", default=200000, type=int, help="number of messages") 17 | parser.add_argument( 18 | "--times", default=1, type=int, help="number of times to run the test" 19 | ) 20 | parser.add_argument("--workers", default=3, type=int, help="number of workers") 21 | parser.add_argument( 22 | "--addr", default="127.0.0.1:25000", type=str, help="address:port of echoserver" 23 | ) 24 | parser.add_argument("--ssl", default=False, action="store_true") 25 | args = parser.parse_args() 26 | 27 | client_context = None 28 | if args.ssl: 29 | print("with SSL") 30 | if hasattr(ssl, "PROTOCOL_TLS"): 31 | client_context = ssl.SSLContext(ssl.PROTOCOL_TLS) 32 | else: 33 | client_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 34 | if hasattr(client_context, "check_hostname"): 35 | client_context.check_hostname = False 36 | client_context.verify_mode = ssl.CERT_NONE 37 | 38 | unix = False 39 | if args.addr.startswith("file:"): 40 | unix = True 41 | addr = args.addr[5:] 42 | else: 43 | addr = args.addr.split(":") 44 | addr[1] = int(addr[1]) 45 | addr = tuple(addr) 46 | print("will connect to: {}".format(addr)) 47 | 48 | MSGSIZE = args.msize 49 | REQSIZE = MSGSIZE * args.mpr 50 | 51 | msg = b"x" * (MSGSIZE - 1) + b"\n" 52 | if args.mpr: 53 | msg *= args.mpr 54 | 55 | def run_test(n): 56 | print("Sending", NMESSAGES, "messages") 57 | if args.mpr: 58 | n //= args.mpr 59 | 60 | if unix: 61 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 62 | else: 63 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 64 | 65 | try: 66 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 67 | except (OSError, NameError): 68 | pass 69 | 70 | if client_context: 71 | sock = client_context.wrap_socket(sock) 72 | 73 | sock.connect(addr) 74 | 75 | while n > 0: 76 | sock.sendall(msg) 77 | nrecv = 0 78 | while nrecv < REQSIZE: 79 | resp = sock.recv(REQSIZE) 80 | if not resp: 81 | raise SystemExit() 82 | nrecv += len(resp) 83 | n -= 1 84 | 85 | TIMES = args.times 86 | N = args.workers 87 | NMESSAGES = args.num 88 | start = time.time() 89 | for _ in range(TIMES): 90 | # Winloop comment: switch from ProcessPoolExecutor to 91 | # ThreadPoolExecutor to avoid issues with __mp_main__ on Windows. 92 | with concurrent.futures.ThreadPoolExecutor(max_workers=N) as e: 93 | for _ in range(N): 94 | e.submit(run_test, NMESSAGES) 95 | end = time.time() 96 | duration = end - start 97 | print(NMESSAGES * N * TIMES, "in", duration) 98 | print(NMESSAGES * N * TIMES / duration, "requests/sec") 99 | -------------------------------------------------------------------------------- /winloop/errors.pyx: -------------------------------------------------------------------------------- 1 | cdef __convert_python_error(int uverr): 2 | # XXX Won't work for Windows: 3 | # From libuv docs: 4 | # Implementation detail: on Unix error codes are the 5 | # negated errno (or -errno), while on Windows they 6 | # are defined by libuv to arbitrary negative numbers. 7 | 8 | # Winloop comment: The following approach seems to work for Windows: 9 | # translation from uverr, which is a negative number like -4088 or -4071 10 | # defined by libuv (as mentioned above), to error numbers obtained via 11 | # the Python module errno. 12 | err = getattr(errno, uv.uv_err_name(uverr).decode(), uverr) 13 | return OSError(err, uv.uv_strerror(uverr).decode()) 14 | 15 | # TODO: Create a switch block for dealing with this otherwise we're waiting on match blocks 16 | # to be fully implemented 17 | cdef int __convert_socket_error(int uverr): 18 | cdef int sock_err = 0 19 | 20 | if uverr == uv.UV_EAI_ADDRFAMILY: 21 | sock_err = socket_EAI_ADDRFAMILY 22 | 23 | elif uverr == uv.UV_EAI_AGAIN: 24 | sock_err = socket_EAI_AGAIN 25 | 26 | elif uverr == uv.UV_EAI_BADFLAGS: 27 | sock_err = socket_EAI_BADFLAGS 28 | 29 | elif uverr == uv.UV_EAI_BADHINTS: 30 | sock_err = socket_EAI_BADHINTS 31 | 32 | elif uverr == uv.UV_EAI_CANCELED: 33 | sock_err = socket_EAI_CANCELED 34 | 35 | elif uverr == uv.UV_EAI_FAIL: 36 | sock_err = socket_EAI_FAIL 37 | 38 | elif uverr == uv.UV_EAI_FAMILY: 39 | sock_err = socket_EAI_FAMILY 40 | 41 | elif uverr == uv.UV_EAI_MEMORY: 42 | sock_err = socket_EAI_MEMORY 43 | 44 | elif uverr == uv.UV_EAI_NODATA: 45 | sock_err = socket_EAI_NODATA 46 | 47 | elif uverr == uv.UV_EAI_NONAME: 48 | sock_err = socket_EAI_NONAME 49 | 50 | elif uverr == uv.UV_EAI_OVERFLOW: 51 | sock_err = socket_EAI_OVERFLOW 52 | 53 | elif uverr == uv.UV_EAI_PROTOCOL: 54 | sock_err = socket_EAI_PROTOCOL 55 | 56 | elif uverr == uv.UV_EAI_SERVICE: 57 | sock_err = socket_EAI_SERVICE 58 | 59 | elif uverr == uv.UV_EAI_SOCKTYPE: 60 | sock_err = socket_EAI_SOCKTYPE 61 | 62 | return sock_err 63 | 64 | 65 | cdef convert_error(int uverr): 66 | cdef int sock_err 67 | 68 | if uverr == uv.UV_ECANCELED: 69 | return aio_CancelledError() 70 | 71 | sock_err = __convert_socket_error(uverr) 72 | if sock_err: 73 | # Winloop comment: Sometimes libraries will throw in some 74 | # unwanted unicode BS to unravel, to prevent the possibility of this being a threat, 75 | # surrogateescape is utilized 76 | # SEE: https://github.com/Vizonex/Winloop/issues/32 77 | msg = system.gai_strerror(sock_err).decode('utf-8', "surrogateescape") 78 | # Winloop comment: on Windows, cPython has a simpler error 79 | # message than uvlib (via winsock probably) in these two cases: 80 | # EAI_FAMILY [ErrNo 10047] "An address incompatible with the requested protocol was used. " 81 | # EAI_NONAME [ErrNo 10001] "No such host is known. " 82 | # We replace these messages with "getaddrinfo failed" 83 | if sys.platform == 'win32': 84 | if sock_err in (socket_EAI_FAMILY, socket_EAI_NONAME): 85 | msg = 'getaddrinfo failed' 86 | return socket_gaierror(sock_err, msg) 87 | 88 | return __convert_python_error(uverr) 89 | 90 | -------------------------------------------------------------------------------- /tests/test_regr1.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import multiprocessing 3 | import queue 4 | import signal 5 | import threading 6 | import unittest 7 | 8 | import winloop as uvloop 9 | from winloop import _testbase as tb 10 | 11 | 12 | class EchoServerProtocol(asyncio.Protocol): 13 | def connection_made(self, transport): 14 | transport.write(b"z") 15 | 16 | 17 | class EchoClientProtocol(asyncio.Protocol): 18 | def __init__(self, loop): 19 | self.loop = loop 20 | 21 | def connection_made(self, transport): 22 | self.transport = transport 23 | 24 | def data_received(self, data): 25 | self.transport.close() 26 | 27 | def connection_lost(self, exc): 28 | self.loop.stop() 29 | 30 | 31 | class FailedTestError(BaseException): 32 | pass 33 | 34 | 35 | def run_server(quin, qout): 36 | server_loop = None 37 | 38 | def server_thread(): 39 | nonlocal server_loop 40 | loop = server_loop = uvloop.new_event_loop() 41 | asyncio.set_event_loop(loop) 42 | coro = loop.create_server(EchoServerProtocol, "127.0.0.1", 0) 43 | server = loop.run_until_complete(coro) 44 | addr = server.sockets[0].getsockname() 45 | qout.put(addr) 46 | loop.run_forever() 47 | server.close() 48 | loop.run_until_complete(server.wait_closed()) 49 | try: 50 | loop.close() 51 | except Exception as exc: 52 | print(exc) 53 | qout.put("stopped") 54 | 55 | thread = threading.Thread(target=server_thread, daemon=True) 56 | thread.start() 57 | 58 | quin.get() 59 | server_loop.call_soon_threadsafe(server_loop.stop) 60 | thread.join(1) 61 | 62 | 63 | class TestIssue39Regr(tb.UVTestCase): 64 | """See https://github.com/MagicStack/uvloop/issues/39 for details. 65 | 66 | Original code to reproduce the bug is by Jim Fulton. 67 | """ 68 | 69 | def on_alarm(self, sig, fr): 70 | if self.running: 71 | raise FailedTestError 72 | 73 | def run_test(self): 74 | for i in range(10): 75 | for threaded in [True, False]: 76 | if threaded: 77 | qin, qout = queue.Queue(), queue.Queue() 78 | threading.Thread( 79 | target=run_server, args=(qin, qout), daemon=True 80 | ).start() 81 | else: 82 | qin = multiprocessing.Queue() 83 | qout = multiprocessing.Queue() 84 | multiprocessing.Process( 85 | target=run_server, args=(qin, qout), daemon=True 86 | ).start() 87 | 88 | addr = qout.get() 89 | loop = self.new_loop() 90 | asyncio.set_event_loop(loop) 91 | loop.create_task( 92 | loop.create_connection( 93 | lambda: EchoClientProtocol(loop), host=addr[0], port=addr[1] 94 | ) 95 | ) 96 | loop.run_forever() 97 | loop.close() 98 | qin.put("stop") 99 | qout.get() 100 | 101 | @unittest.skipIf( 102 | multiprocessing.get_start_method(False) == "spawn", 103 | "no need to test on Windows and macOS where spawn is used instead of fork", 104 | ) 105 | def test_issue39_regression(self): 106 | signal.signal(signal.SIGALRM, self.on_alarm) 107 | signal.alarm(5) 108 | 109 | try: 110 | self.running = True 111 | self.run_test() 112 | except FailedTestError: 113 | self.fail("deadlocked in libuv") 114 | finally: 115 | self.running = False 116 | signal.signal(signal.SIGALRM, signal.SIG_IGN) 117 | -------------------------------------------------------------------------------- /tests/test_fs_event.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os.path 3 | import tempfile 4 | import unittest 5 | 6 | from winloop import _testbase as tb 7 | from winloop.loop import FileSystemEvent 8 | 9 | 10 | class Test_UV_FS_EVENT_CHANGE(tb.UVTestCase): 11 | async def _file_writer(self): 12 | f = await self.q.get() 13 | while True: 14 | f.write("hello uvloop\n") 15 | f.flush() 16 | # Winloop comment: insertion of os.fsync() call 17 | # needed on Windows (Windows 10, Windows 11) to 18 | # trigger change events; also works with Linux, 19 | # but need to double check this. 20 | os.fsync(f.fileno()) 21 | x = await self.q.get() 22 | if x is None: 23 | return 24 | 25 | def fs_event_setup(self): 26 | self.change_event_count = 0 27 | self.fname = "" 28 | self.q = asyncio.Queue() 29 | 30 | def event_cb(self, ev_fname: bytes, evt: FileSystemEvent): 31 | _d, fn = os.path.split(self.fname) 32 | self.assertEqual(ev_fname, fn) 33 | self.assertEqual(evt, FileSystemEvent.CHANGE) 34 | self.change_event_count += 1 35 | if self.change_event_count < 4: 36 | self.q.put_nowait(0) 37 | else: 38 | self.q.put_nowait(None) 39 | 40 | @unittest.skip("broken") 41 | def test_fs_event_change(self): 42 | self.fs_event_setup() 43 | 44 | async def run(write_task): 45 | self.q.put_nowait(tf) 46 | try: 47 | await asyncio.wait_for(write_task, 4) 48 | except asyncio.TimeoutError: 49 | write_task.cancel() 50 | 51 | with tempfile.NamedTemporaryFile("wt") as tf: 52 | self.fname = tf.name.encode() 53 | h = self.loop._monitor_fs(tf.name, self.event_cb) 54 | self.assertFalse(h.cancelled()) 55 | 56 | self.loop.run_until_complete( 57 | run(self.loop.create_task(self._file_writer())) 58 | ) 59 | h.cancel() 60 | self.assertTrue(h.cancelled()) 61 | 62 | self.assertEqual(self.change_event_count, 4) 63 | 64 | 65 | class Test_UV_FS_EVENT_RENAME(tb.UVTestCase): 66 | async def _file_renamer(self): 67 | await self.q.get() 68 | os.rename( 69 | os.path.join(self.dname, self.changed_name), 70 | os.path.join(self.dname, self.changed_name + "-new"), 71 | ) 72 | await self.q.get() 73 | 74 | def fs_event_setup(self): 75 | self.dname = "" 76 | self.changed_name = "hello_fs_event.txt" 77 | self.changed_set = {self.changed_name, self.changed_name + "-new"} 78 | self.q = asyncio.Queue() 79 | 80 | def event_cb(self, ev_fname: bytes, evt: FileSystemEvent): 81 | ev_fname = ev_fname.decode() 82 | self.assertEqual(evt, FileSystemEvent.RENAME) 83 | self.changed_set.remove(ev_fname) 84 | if len(self.changed_set) == 0: 85 | self.q.put_nowait(None) 86 | 87 | def test_fs_event_rename(self): 88 | self.fs_event_setup() 89 | 90 | async def run(write_task): 91 | self.q.put_nowait(0) 92 | try: 93 | await asyncio.wait_for(write_task, 4) 94 | except asyncio.TimeoutError: 95 | write_task.cancel() 96 | 97 | with tempfile.TemporaryDirectory() as td_name: 98 | self.dname = td_name 99 | f = open(os.path.join(td_name, self.changed_name), "wt") 100 | f.write("hello!") 101 | f.close() 102 | h = self.loop._monitor_fs(td_name, self.event_cb) 103 | self.assertFalse(h.cancelled()) 104 | 105 | self.loop.run_until_complete( 106 | run(self.loop.create_task(self._file_renamer())) 107 | ) 108 | h.cancel() 109 | self.assertTrue(h.cancelled()) 110 | 111 | self.assertEqual(len(self.changed_set), 0) 112 | -------------------------------------------------------------------------------- /winloop/sslproto.pxd: -------------------------------------------------------------------------------- 1 | cdef enum SSLProtocolState: 2 | UNWRAPPED = 0 3 | DO_HANDSHAKE = 1 4 | WRAPPED = 2 5 | FLUSHING = 3 6 | SHUTDOWN = 4 7 | 8 | 9 | cdef enum AppProtocolState: 10 | # This tracks the state of app protocol (https://git.io/fj59P): 11 | # 12 | # INIT -cm-> CON_MADE [-dr*->] [-er-> EOF?] -cl-> CON_LOST 13 | # 14 | # * cm: connection_made() 15 | # * dr: data_received() 16 | # * er: eof_received() 17 | # * cl: connection_lost() 18 | 19 | STATE_INIT = 0 20 | STATE_CON_MADE = 1 21 | STATE_EOF = 2 22 | STATE_CON_LOST = 3 23 | 24 | 25 | cdef class _SSLProtocolTransport: 26 | cdef: 27 | Loop _loop 28 | SSLProtocol _ssl_protocol 29 | bint _closed 30 | object context 31 | 32 | 33 | cdef class SSLProtocol: 34 | cdef: 35 | bint _server_side 36 | str _server_hostname 37 | object _sslcontext 38 | 39 | object _extra 40 | 41 | object _write_backlog 42 | size_t _write_buffer_size 43 | 44 | object _waiter 45 | Loop _loop 46 | _SSLProtocolTransport _app_transport 47 | bint _app_transport_created 48 | 49 | object _transport 50 | object _ssl_handshake_timeout 51 | object _ssl_shutdown_timeout 52 | 53 | object _sslobj 54 | object _sslobj_read 55 | object _sslobj_write 56 | object _incoming 57 | object _incoming_write 58 | object _outgoing 59 | object _outgoing_read 60 | char* _ssl_buffer 61 | size_t _ssl_buffer_len 62 | object _ssl_buffer_view 63 | SSLProtocolState _state 64 | size_t _conn_lost 65 | AppProtocolState _app_state 66 | 67 | bint _ssl_writing_paused 68 | bint _app_reading_paused 69 | 70 | size_t _incoming_high_water 71 | size_t _incoming_low_water 72 | bint _ssl_reading_paused 73 | 74 | bint _app_writing_paused 75 | size_t _outgoing_high_water 76 | size_t _outgoing_low_water 77 | 78 | object _app_protocol 79 | bint _app_protocol_is_buffer 80 | object _app_protocol_get_buffer 81 | object _app_protocol_buffer_updated 82 | 83 | object _handshake_start_time 84 | object _handshake_timeout_handle 85 | object _shutdown_timeout_handle 86 | 87 | cdef _set_app_protocol(self, app_protocol) 88 | cdef _wakeup_waiter(self, exc=*) 89 | cdef _get_extra_info(self, name, default=*) 90 | cdef _set_state(self, SSLProtocolState new_state) 91 | 92 | # Handshake flow 93 | 94 | cdef _start_handshake(self) 95 | cdef _check_handshake_timeout(self) 96 | cdef _do_handshake(self) 97 | cdef _on_handshake_complete(self, handshake_exc) 98 | 99 | # Shutdown flow 100 | 101 | cdef _start_shutdown(self, object context=*) 102 | cdef _check_shutdown_timeout(self) 103 | cdef _do_read_into_void(self, object context) 104 | cdef _do_flush(self, object context=*) 105 | cdef _do_shutdown(self, object context=*) 106 | cdef _on_shutdown_complete(self, shutdown_exc) 107 | cdef _abort(self, exc) 108 | 109 | # Outgoing flow 110 | 111 | cdef _write_appdata(self, list_of_data, object context) 112 | cdef _do_write(self) 113 | cdef _process_outgoing(self) 114 | 115 | # Incoming flow 116 | 117 | cdef _do_read(self) 118 | cdef _do_read__buffered(self) 119 | cdef _do_read__copied(self) 120 | cdef _call_eof_received(self, object context=*) 121 | 122 | # Flow control for writes from APP socket 123 | 124 | cdef _control_app_writing(self, object context=*) 125 | cdef size_t _get_write_buffer_size(self) 126 | cdef _set_write_buffer_limits(self, high=*, low=*) 127 | 128 | # Flow control for reads to APP socket 129 | 130 | cdef _pause_reading(self) 131 | cdef _resume_reading(self, object context) 132 | 133 | # Flow control for reads from SSL socket 134 | 135 | cdef _control_ssl_reading(self) 136 | cdef _set_read_buffer_limits(self, high=*, low=*) 137 | cdef size_t _get_read_buffer_size(self) 138 | cdef _fatal_error(self, exc, message=*) 139 | -------------------------------------------------------------------------------- /winloop/server.pyx: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | cdef class Server: 5 | def __cinit__(self, Loop loop): 6 | self._loop = loop 7 | self._servers = [] 8 | self._waiters = [] 9 | self._active_count = 0 10 | self._serving_forever_fut = None 11 | 12 | cdef _add_server(self, UVStreamServer srv): 13 | self._servers.append(srv) 14 | 15 | cdef _start_serving(self): 16 | if self._serving: 17 | return 18 | 19 | self._serving = 1 20 | for server in self._servers: 21 | (server).listen() 22 | 23 | cdef _wakeup(self): 24 | cdef list waiters 25 | 26 | waiters = self._waiters 27 | self._waiters = None 28 | for waiter in waiters: 29 | if not waiter.done(): 30 | waiter.set_result(waiter) 31 | 32 | cdef _attach(self): 33 | assert self._servers is not None 34 | self._active_count += 1 35 | 36 | cdef _detach(self): 37 | assert self._active_count > 0 38 | self._active_count -= 1 39 | if self._active_count == 0 and self._servers is None: 40 | self._wakeup() 41 | 42 | cdef _ref(self): 43 | # Keep the server object alive while it's not explicitly closed. 44 | self._loop._servers.add(self) 45 | 46 | cdef _unref(self): 47 | self._loop._servers.discard(self) 48 | 49 | # Public API 50 | 51 | @cython.iterable_coroutine 52 | async def __aenter__(self): 53 | return self 54 | 55 | @cython.iterable_coroutine 56 | async def __aexit__(self, *exc): 57 | self.close() 58 | await self.wait_closed() 59 | 60 | def __repr__(self): 61 | return '<%s sockets=%r>' % (self.__class__.__name__, self.sockets) 62 | 63 | def get_loop(self): 64 | return self._loop 65 | 66 | @cython.iterable_coroutine 67 | async def wait_closed(self): 68 | # Do not remove `self._servers is None` below 69 | # because close() method only closes server sockets 70 | # and existing client connections are left open. 71 | if self._servers is None or self._waiters is None: 72 | return 73 | waiter = self._loop._new_future() 74 | self._waiters.append(waiter) 75 | await waiter 76 | 77 | def close(self): 78 | cdef list servers 79 | 80 | if self._servers is None: 81 | return 82 | 83 | try: 84 | servers = self._servers 85 | self._servers = None 86 | self._serving = 0 87 | 88 | for server in servers: 89 | (server)._close() 90 | 91 | if self._active_count == 0: 92 | self._wakeup() 93 | finally: 94 | self._unref() 95 | 96 | def is_serving(self): 97 | return self._serving 98 | 99 | @cython.iterable_coroutine 100 | async def start_serving(self): 101 | self._start_serving() 102 | 103 | @cython.iterable_coroutine 104 | async def serve_forever(self): 105 | if self._serving_forever_fut is not None: 106 | raise RuntimeError( 107 | f'server {self!r} is already being awaited on serve_forever()') 108 | if self._servers is None: 109 | raise RuntimeError(f'server {self!r} is closed') 110 | 111 | self._start_serving() 112 | self._serving_forever_fut = self._loop.create_future() 113 | 114 | try: 115 | await self._serving_forever_fut 116 | except asyncio.CancelledError: 117 | try: 118 | self.close() 119 | await self.wait_closed() 120 | finally: 121 | raise 122 | finally: 123 | self._serving_forever_fut = None 124 | 125 | property sockets: 126 | def __get__(self): 127 | cdef list sockets = [] 128 | 129 | # Guard against `self._servers is None` 130 | if self._servers: 131 | for server in self._servers: 132 | sockets.append( 133 | (server)._get_socket() 134 | ) 135 | 136 | return sockets 137 | -------------------------------------------------------------------------------- /tests/test_process_spawning.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import ctypes.util 3 | import logging 4 | import sys 5 | from concurrent.futures import ThreadPoolExecutor 6 | from threading import Thread 7 | from unittest import TestCase 8 | 9 | import winloop as uvloop 10 | 11 | 12 | class ProcessSpawningTestCollection(TestCase): 13 | def test_spawning_external_process(self): 14 | """Test spawning external process (using `popen` system call) that 15 | cause loop freeze.""" 16 | 17 | async def run(loop): 18 | event = asyncio.Event() 19 | 20 | dummy_workers = [simulate_loop_activity(loop, event) for _ in range(5)] 21 | spawn_worker = spawn_external_process(loop, event) 22 | done, pending = await asyncio.wait( 23 | [asyncio.ensure_future(fut) for fut in ([spawn_worker] + dummy_workers)] 24 | ) 25 | exceptions = [result.exception() for result in done if result.exception()] 26 | if exceptions: 27 | raise exceptions[0] 28 | 29 | return True 30 | 31 | async def simulate_loop_activity(loop, done_event): 32 | """Simulate loop activity by busy waiting for event.""" 33 | while True: 34 | try: 35 | await asyncio.wait_for(done_event.wait(), timeout=0.1) 36 | except asyncio.TimeoutError: 37 | pass 38 | 39 | if done_event.is_set(): 40 | return None 41 | 42 | async def spawn_external_process(loop, event): 43 | executor = ThreadPoolExecutor() 44 | try: 45 | call = loop.run_in_executor(executor, spawn_process) 46 | await asyncio.wait_for(call, timeout=3600) 47 | finally: 48 | event.set() 49 | executor.shutdown(wait=False) 50 | return True 51 | 52 | BUFFER_LENGTH = 1025 53 | BufferType = ctypes.c_char * (BUFFER_LENGTH - 1) 54 | 55 | def run_echo(popen, fread, pclose): 56 | fd = popen("echo test".encode("ASCII"), "r".encode("ASCII")) 57 | try: 58 | while True: 59 | buffer = BufferType() 60 | data = ctypes.c_void_p(ctypes.addressof(buffer)) 61 | 62 | # -> this call will freeze whole loop in case of bug 63 | read = fread(data, 1, BUFFER_LENGTH, fd) 64 | if not read: 65 | break 66 | except Exception: 67 | logging.getLogger().exception("read error") 68 | raise 69 | finally: 70 | pclose(fd) 71 | 72 | def spawn_process(): 73 | """Spawn external process via `popen` system call.""" 74 | 75 | # WINLOOP comment: use 'msvcrt' instead of 'c', and 76 | # attrbs '_popen' and '_plocse' instead of 'popen' and 'pclose'. 77 | # NB: this test turns out to take close to 10x longer on Windows?! 78 | stdio = ctypes.CDLL( 79 | ctypes.util.find_library("msvcrt" if sys.platform == "win32" else "c") 80 | ) 81 | 82 | # popen system call 83 | popen = stdio._popen if sys.platform == "win32" else stdio.popen 84 | popen.argtypes = (ctypes.c_char_p, ctypes.c_char_p) 85 | popen.restype = ctypes.c_void_p 86 | 87 | # pclose system call 88 | pclose = stdio._pclose if sys.platform == "win32" else stdio.pclose 89 | pclose.argtypes = (ctypes.c_void_p,) 90 | pclose.restype = ctypes.c_int 91 | 92 | # fread system call 93 | fread = stdio.fread 94 | fread.argtypes = ( 95 | ctypes.c_void_p, 96 | ctypes.c_size_t, 97 | ctypes.c_size_t, 98 | ctypes.c_void_p, 99 | ) 100 | fread.restype = ctypes.c_size_t 101 | 102 | for iteration in range(1000): 103 | t = Thread(target=run_echo, args=(popen, fread, pclose), daemon=True) 104 | t.start() 105 | t.join(timeout=10.0) 106 | if t.is_alive(): 107 | raise Exception("process freeze detected at {}".format(iteration)) 108 | 109 | return True 110 | 111 | loop = uvloop.new_event_loop() 112 | proc = loop.run_until_complete(run(loop)) 113 | self.assertTrue(proc) 114 | -------------------------------------------------------------------------------- /winloop/includes/compat.h: -------------------------------------------------------------------------------- 1 | #ifndef __WINLOOP_COMPAT_H__ 2 | #define __WINLOOP_COMPAT_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #ifndef _WIN32 8 | #include 9 | #include 10 | #include 11 | #include 12 | #else 13 | #include 14 | #include 15 | #endif 16 | #include "Python.h" 17 | #include "uv.h" 18 | 19 | 20 | #ifndef EWOULDBLOCK 21 | #define EWOULDBLOCK EAGAIN 22 | #endif 23 | 24 | #ifdef __APPLE__ 25 | #define PLATFORM_IS_APPLE 1 26 | #else 27 | #define PLATFORM_IS_APPLE 0 28 | #endif 29 | 30 | 31 | #ifdef __linux__ 32 | # define PLATFORM_IS_LINUX 1 33 | # include 34 | #else 35 | # define PLATFORM_IS_LINUX 0 36 | #endif 37 | 38 | #ifdef __APPLE__ 39 | # define EPOLL_CTL_DEL 2 40 | struct epoll_event {}; 41 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) { 42 | return 0; 43 | }; 44 | #endif 45 | 46 | #ifdef _WIN32 47 | # define EPOLL_CTL_DEL 2 48 | /* error C2016: C requires that a struct or union have at least one member on Windows 49 | with default compilation flags. Therefore put dummy field for now. */ 50 | struct epoll_event {int dummyfield;}; 51 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) { 52 | return 0; 53 | }; 54 | #endif 55 | 56 | #ifdef _WIN32 57 | int SIGCHLD = 0; 58 | int SO_REUSEPORT = 0; 59 | 60 | struct sockaddr_un {unsigned short sun_family; char* sun_path;}; 61 | 62 | int socketpair(int domain, int type, int protocol, int socket_vector[2]) { 63 | return 0; 64 | } 65 | 66 | /* redefine write as counterpart of unistd.h/write */ 67 | int write(int fd, const void *buf, unsigned int count) { 68 | WSABUF wsa; 69 | unsigned long dbytes; 70 | wsa.buf = (char*)buf; 71 | wsa.len = (unsigned long)count; 72 | errno = WSASend(fd, &wsa, 1, &dbytes, 0, NULL, NULL); 73 | if (errno == SOCKET_ERROR) { 74 | errno = WSAGetLastError(); 75 | if (errno == 10035) 76 | errno = EAGAIN; 77 | return -1; 78 | } 79 | else 80 | return dbytes; 81 | } 82 | #endif 83 | 84 | PyObject * 85 | MakeUnixSockPyAddr(struct sockaddr_un *addr) 86 | { 87 | #ifdef _WIN32 88 | return NULL; 89 | #else 90 | if (addr->sun_family != AF_UNIX) { 91 | PyErr_SetString( 92 | PyExc_ValueError, "a UNIX socket addr was expected"); 93 | return NULL; 94 | } 95 | 96 | #ifdef __linux__ 97 | int addrlen = sizeof (struct sockaddr_un); 98 | size_t linuxaddrlen = addrlen - offsetof(struct sockaddr_un, sun_path); 99 | if (linuxaddrlen > 0 && addr->sun_path[0] == 0) { 100 | return PyBytes_FromStringAndSize(addr->sun_path, linuxaddrlen); 101 | } 102 | else 103 | #endif /* linux */ 104 | { 105 | /* regular NULL-terminated string */ 106 | return PyUnicode_DecodeFSDefault(addr->sun_path); 107 | } 108 | #endif /* _WIN32 */ 109 | } 110 | 111 | #ifdef _WIN32 112 | #define PLATFORM_IS_WINDOWS 1 113 | int getuid() { 114 | return 0; 115 | } 116 | #else 117 | #define PLATFORM_IS_WINDOWS 0 118 | #endif 119 | 120 | 121 | #if PY_VERSION_HEX < 0x03070100 122 | 123 | PyObject * Context_CopyCurrent(void) { 124 | return (PyObject *)PyContext_CopyCurrent(); 125 | }; 126 | 127 | int Context_Enter(PyObject *ctx) { 128 | return PyContext_Enter((PyContext *)ctx); 129 | } 130 | 131 | int Context_Exit(PyObject *ctx) { 132 | return PyContext_Exit((PyContext *)ctx); 133 | } 134 | 135 | #else 136 | 137 | PyObject * Context_CopyCurrent(void) { 138 | return PyContext_CopyCurrent(); 139 | }; 140 | 141 | int Context_Enter(PyObject *ctx) { 142 | return PyContext_Enter(ctx); 143 | } 144 | 145 | int Context_Exit(PyObject *ctx) { 146 | return PyContext_Exit(ctx); 147 | } 148 | 149 | #endif 150 | 151 | /* inlined from cpython/Modules/signalmodule.c 152 | * https://github.com/python/cpython/blob/v3.13.0a6/Modules/signalmodule.c#L1931-L1951 153 | * private _Py_RestoreSignals has been moved to CPython internals in Python 3.13 154 | * https://github.com/python/cpython/pull/106400 */ 155 | 156 | void 157 | _Py_RestoreSignals(void) 158 | { 159 | #ifdef SIGPIPE 160 | PyOS_setsig(SIGPIPE, SIG_DFL); 161 | #endif 162 | #ifdef SIGXFZ 163 | PyOS_setsig(SIGXFZ, SIG_DFL); 164 | #endif 165 | #ifdef SIGXFSZ 166 | PyOS_setsig(SIGXFSZ, SIG_DFL); 167 | #endif 168 | } 169 | 170 | #ifdef _WIN32 171 | void PyOS_BeforeFork() { 172 | return; 173 | } 174 | void PyOS_AfterFork_Parent() { 175 | return; 176 | } 177 | void PyOS_AfterFork_Child() { 178 | return; 179 | } 180 | 181 | #endif 182 | 183 | // TODO: all versions of _PyEval_EvalFrameDefault so we can get rid of _noop.noop 184 | // which would be a massive performance enhancement and allow pyinstaller to compile 3.9 -> 3.14 185 | 186 | 187 | #endif // __WINLOOP_COMPAT_H__ -------------------------------------------------------------------------------- /tests/test_aiohttp.py: -------------------------------------------------------------------------------- 1 | try: 2 | import aiohttp 3 | import aiohttp.web 4 | except ImportError: 5 | skip_tests = True 6 | else: 7 | skip_tests = False 8 | 9 | import asyncio 10 | import sys 11 | import unittest 12 | import weakref 13 | 14 | from winloop import _testbase as tb 15 | 16 | # TODO: Fix or patch DeprecationWarning 17 | 18 | 19 | class _TestAioHTTP: 20 | def test_aiohttp_basic_1(self): 21 | PAYLOAD = "

It Works!

" * 10000 22 | 23 | async def on_request(request): 24 | return aiohttp.web.Response(text=PAYLOAD) 25 | 26 | asyncio.set_event_loop(self.loop) 27 | app = aiohttp.web.Application() 28 | app.router.add_get("/", on_request) 29 | 30 | runner = aiohttp.web.AppRunner(app) 31 | self.loop.run_until_complete(runner.setup()) 32 | site = aiohttp.web.TCPSite(runner, "0.0.0.0", "0") 33 | self.loop.run_until_complete(site.start()) 34 | port = site._server.sockets[0].getsockname()[1] 35 | 36 | async def test(): 37 | # Make sure we're using the correct event loop. 38 | self.assertIs(asyncio.get_event_loop(), self.loop) 39 | 40 | for addr in (("localhost", port), ("127.0.0.1", port)): 41 | async with aiohttp.ClientSession() as client: 42 | async with client.get("http://{}:{}".format(*addr)) as r: 43 | self.assertEqual(r.status, 200) 44 | result = await r.text() 45 | self.assertEqual(result, PAYLOAD) 46 | 47 | self.loop.run_until_complete(test()) 48 | self.loop.run_until_complete(runner.cleanup()) 49 | 50 | def test_aiohttp_graceful_shutdown(self): 51 | # NOTE: This Might Already be solved I haven't checked yet - Vizonex 52 | if sys.version_info == (3, 12): 53 | # In Python 3.12.0, asyncio.Server.wait_closed() waits for all 54 | # existing connections to complete, before aiohttp sends 55 | # on_shutdown signals. 56 | # https://github.com/aio-libs/aiohttp/issues/7675#issuecomment-1752143748 57 | # https://github.com/python/cpython/pull/98582 58 | raise unittest.SkipTest("bug in aiohttp: #7675") 59 | 60 | async def websocket_handler(request): 61 | ws = aiohttp.web.WebSocketResponse() 62 | await ws.prepare(request) 63 | request.app["websockets"].add(ws) 64 | try: 65 | async for msg in ws: 66 | await ws.send_str(msg.data) 67 | finally: 68 | request.app["websockets"].discard(ws) 69 | return ws 70 | 71 | async def on_shutdown(app): 72 | for ws in set(app["websockets"]): 73 | await ws.close( 74 | code=aiohttp.WSCloseCode.GOING_AWAY, message="Server shutdown" 75 | ) 76 | 77 | asyncio.set_event_loop(self.loop) 78 | app = aiohttp.web.Application() 79 | app.router.add_get("/", websocket_handler) 80 | app.on_shutdown.append(on_shutdown) 81 | app["websockets"] = weakref.WeakSet() 82 | 83 | runner = aiohttp.web.AppRunner(app) 84 | self.loop.run_until_complete(runner.setup()) 85 | site = aiohttp.web.TCPSite( 86 | runner, 87 | "0.0.0.0", 88 | 0, 89 | # https://github.com/aio-libs/aiohttp/pull/7188 90 | shutdown_timeout=0.1, 91 | ) 92 | self.loop.run_until_complete(site.start()) 93 | port = site._server.sockets[0].getsockname()[1] 94 | 95 | async def client(): 96 | async with aiohttp.ClientSession() as client: 97 | async with client.ws_connect("http://127.0.0.1:{}".format(port)) as ws: 98 | await ws.send_str("hello") 99 | async for msg in ws: 100 | assert msg.data == "hello" 101 | 102 | client_task = asyncio.ensure_future(client()) 103 | 104 | async def stop(): 105 | await asyncio.sleep(0.1) 106 | try: 107 | await asyncio.wait_for(runner.cleanup(), timeout=0.5) 108 | finally: 109 | try: 110 | client_task.cancel() 111 | await client_task 112 | except asyncio.CancelledError: 113 | pass 114 | 115 | self.loop.run_until_complete(stop()) 116 | 117 | 118 | @unittest.skipIf(skip_tests, "no aiohttp module") 119 | class Test_UV_AioHTTP(_TestAioHTTP, tb.UVTestCase): 120 | pass 121 | 122 | 123 | @unittest.skipIf(skip_tests, "no aiohttp module") 124 | class Test_AIO_AioHTTP(_TestAioHTTP, tb.AIOTestCase): 125 | # Winloop comment: switching to selector loop (instead of proactor), 126 | # see https://github.com/saghul/aiodns/issues/86 127 | if sys.platform == "win32": 128 | 129 | def new_policy(self): 130 | return asyncio.WindowsSelectorEventLoopPolicy() 131 | 132 | 133 | if __name__ == "__main__": 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /winloop/handles/streamserver.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVStreamServer(UVSocketHandle): 3 | 4 | def __cinit__(self): 5 | self.opened = 0 6 | self._server = None 7 | self.ssl = None 8 | self.ssl_handshake_timeout = None 9 | self.ssl_shutdown_timeout = None 10 | self.protocol_factory = None 11 | 12 | cdef inline _init(self, Loop loop, object protocol_factory, 13 | Server server, 14 | object backlog, 15 | object ssl, 16 | object ssl_handshake_timeout, 17 | object ssl_shutdown_timeout): 18 | 19 | if not isinstance(backlog, int): 20 | # Don't allow floats 21 | raise TypeError('integer argument expected, got {}'.format( 22 | type(backlog).__name__)) 23 | 24 | if ssl is not None: 25 | if not isinstance(ssl, ssl_SSLContext): 26 | raise TypeError( 27 | 'ssl is expected to be None or an instance of ' 28 | 'ssl.SSLContext, got {!r}'.format(ssl)) 29 | else: 30 | if ssl_handshake_timeout is not None: 31 | raise ValueError( 32 | 'ssl_handshake_timeout is only meaningful with ssl') 33 | if ssl_shutdown_timeout is not None: 34 | raise ValueError( 35 | 'ssl_shutdown_timeout is only meaningful with ssl') 36 | 37 | self.backlog = backlog 38 | self.ssl = ssl 39 | self.ssl_handshake_timeout = ssl_handshake_timeout 40 | self.ssl_shutdown_timeout = ssl_shutdown_timeout 41 | 42 | self._start_init(loop) 43 | self.protocol_factory = protocol_factory 44 | self._server = server 45 | 46 | cdef inline listen(self): 47 | cdef int err 48 | self._ensure_alive() 49 | 50 | if self.protocol_factory is None: 51 | raise RuntimeError('unable to listen(); no protocol_factory') 52 | 53 | if self.opened != 1: 54 | raise RuntimeError('unopened TCPServer') 55 | 56 | self.context = Context_CopyCurrent() 57 | 58 | err = uv.uv_listen( self._handle, 59 | self.backlog, 60 | __uv_streamserver_on_listen) 61 | if err < 0: 62 | exc = convert_error(err) 63 | self._fatal_error(exc, True) 64 | return 65 | 66 | cdef inline _on_listen(self): 67 | cdef UVStream client 68 | 69 | protocol = run_in_context(self.context, self.protocol_factory) 70 | 71 | if self.ssl is None: 72 | client = self._make_new_transport(protocol, None, self.context) 73 | 74 | else: 75 | waiter = self._loop._new_future() 76 | 77 | ssl_protocol = SSLProtocol( 78 | self._loop, protocol, self.ssl, 79 | waiter, 80 | server_side=True, 81 | server_hostname=None, 82 | ssl_handshake_timeout=self.ssl_handshake_timeout, 83 | ssl_shutdown_timeout=self.ssl_shutdown_timeout) 84 | 85 | client = self._make_new_transport(ssl_protocol, None, self.context) 86 | 87 | waiter.add_done_callback( 88 | ft_partial(self.__on_ssl_connected, client)) 89 | 90 | client._accept(self) 91 | 92 | cdef _fatal_error(self, exc, throw, reason=None): 93 | # Overload UVHandle._fatal_error 94 | 95 | self._close() 96 | 97 | if not isinstance(exc, OSError): 98 | 99 | if throw or self._loop is None: 100 | raise exc 101 | 102 | msg = f'Fatal error on server {self.__class__.__name__}' 103 | if reason is not None: 104 | msg = f'{msg} ({reason})' 105 | 106 | self._loop.call_exception_handler({ 107 | 'message': msg, 108 | 'exception': exc, 109 | }) 110 | 111 | cdef inline _mark_as_open(self): 112 | self.opened = 1 113 | 114 | cdef UVStream _make_new_transport(self, object protocol, object waiter, 115 | object context): 116 | raise NotImplementedError 117 | 118 | def __on_ssl_connected(self, transport, fut): 119 | exc = fut.exception() 120 | if exc is not None: 121 | transport._force_close(exc) 122 | 123 | 124 | cdef void __uv_streamserver_on_listen( 125 | uv.uv_stream_t* handle, 126 | int status, 127 | ) noexcept with gil: 128 | 129 | # callback for uv_listen 130 | 131 | if __ensure_handle_data(handle, 132 | "UVStream listen callback") == 0: 133 | return 134 | 135 | cdef: 136 | UVStreamServer stream = handle.data 137 | 138 | if status < 0: 139 | if UVLOOP_DEBUG: 140 | stream._loop._debug_stream_listen_errors_total += 1 141 | 142 | exc = convert_error(status) 143 | stream._fatal_error( 144 | exc, False, "error status in uv_stream_t.listen callback") 145 | return 146 | 147 | try: 148 | stream._on_listen() 149 | except BaseException as exc: 150 | stream._error(exc, False) 151 | -------------------------------------------------------------------------------- /winloop/pseudosock.pyx: -------------------------------------------------------------------------------- 1 | cdef class PseudoSocket: 2 | cdef: 3 | int _family 4 | int _type 5 | int _proto 6 | int _fd 7 | object _peername 8 | object _sockname 9 | 10 | def __init__(self, int family, int type, int proto, int fd): 11 | self._family = family 12 | self._type = type 13 | self._proto = proto 14 | self._fd = fd 15 | self._peername = None 16 | self._sockname = None 17 | 18 | cdef _na(self, what): 19 | raise TypeError('transport sockets do not support {}'.format(what)) 20 | 21 | cdef _make_sock(self): 22 | return socket_socket(self._family, self._type, self._proto, self._fd) 23 | 24 | property family: 25 | def __get__(self): 26 | try: 27 | return socket_AddressFamily(self._family) 28 | except ValueError: 29 | return self._family 30 | 31 | property type: 32 | def __get__(self): 33 | try: 34 | return socket_SocketKind(self._type) 35 | except ValueError: 36 | return self._type 37 | 38 | property proto: 39 | def __get__(self): 40 | return self._proto 41 | 42 | def __repr__(self): 43 | s = ("_o 146 | Py_INCREF(o) 147 | self._dict_move_to_end(key) # last=True 148 | return o 149 | 150 | cdef inline bint needs_cleanup(self): 151 | return PyODict_SIZE(self._dict) > self._maxsize 152 | 153 | cdef inline cleanup_one(self): 154 | k, _ = self._dict.popitem(last=False) 155 | return k 156 | 157 | def __getitem__(self, key): 158 | o = self._dict[key] 159 | self._dict_move_to_end(key) # last=True 160 | return o 161 | 162 | def __setitem__(self, key, o): 163 | if key in self._dict: 164 | PyODict_SetItem(self._dict, key, o) 165 | self._dict_move_to_end(key) # last=True 166 | else: 167 | PyODict_SetItem(self._dict, key, o) 168 | while self.needs_cleanup(): 169 | self.cleanup_one() 170 | 171 | def __delitem__(self, key): 172 | PyODict_DelItem(self._dict, key) 173 | 174 | def __contains__(self, key): 175 | return PyODict_Contains(self._dict, key) 176 | 177 | def __len__(self): 178 | return PyODict_SIZE(self._dict) 179 | 180 | def __iter__(self): 181 | return iter(self._dict) 182 | -------------------------------------------------------------------------------- /examples/bench/echoserver.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import gc 4 | import os.path 5 | import pathlib 6 | import socket 7 | import ssl 8 | 9 | 10 | PRINT = 0 11 | 12 | 13 | async def echo_server(loop, address, unix): 14 | if unix: 15 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 16 | else: 17 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 19 | sock.bind(address) 20 | sock.listen(5) 21 | sock.setblocking(False) 22 | if PRINT: 23 | print("Server listening at", address) 24 | with sock: 25 | while True: 26 | client, addr = await loop.sock_accept(sock) 27 | if PRINT: 28 | print("Connection from", addr) 29 | loop.create_task(echo_client(loop, client)) 30 | 31 | 32 | async def echo_client(loop, client): 33 | try: 34 | client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 35 | except (OSError, NameError): 36 | pass 37 | 38 | with client: 39 | while True: 40 | data = await loop.sock_recv(client, 1000000) 41 | if not data: 42 | break 43 | await loop.sock_sendall(client, data) 44 | if PRINT: 45 | print("Connection closed") 46 | 47 | 48 | async def echo_client_streams(reader, writer): 49 | sock = writer.get_extra_info("socket") 50 | try: 51 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 52 | except (OSError, NameError): 53 | pass 54 | if PRINT: 55 | print("Connection from", sock.getpeername()) 56 | while True: 57 | data = await reader.read(1000000) 58 | if not data: 59 | break 60 | writer.write(data) 61 | if PRINT: 62 | print("Connection closed") 63 | writer.close() 64 | 65 | 66 | class EchoProtocol(asyncio.Protocol): 67 | def connection_made(self, transport): 68 | self.transport = transport 69 | 70 | def connection_lost(self, exc): 71 | self.transport = None 72 | 73 | def data_received(self, data): 74 | self.transport.write(data) 75 | 76 | 77 | class EchoBufferedProtocol(asyncio.BufferedProtocol): 78 | def connection_made(self, transport): 79 | self.transport = transport 80 | # Here the buffer is intended to be copied, so that the outgoing buffer 81 | # won't be wrongly updated by next read 82 | self.buffer = bytearray(256 * 1024) 83 | 84 | def connection_lost(self, exc): 85 | self.transport = None 86 | 87 | def get_buffer(self, sizehint): 88 | return self.buffer 89 | 90 | def buffer_updated(self, nbytes): 91 | self.transport.write(self.buffer[:nbytes]) 92 | 93 | 94 | async def print_debug(loop): 95 | while True: 96 | print(chr(27) + "[2J") # clear screen 97 | loop.print_debug_info() 98 | await asyncio.sleep(0.5) 99 | 100 | 101 | if __name__ == "__main__": 102 | parser = argparse.ArgumentParser() 103 | parser.add_argument("--uvloop", default=False, action="store_true") 104 | parser.add_argument("--streams", default=False, action="store_true") 105 | parser.add_argument("--proto", default=False, action="store_true") 106 | parser.add_argument("--addr", default="127.0.0.1:25000", type=str) 107 | parser.add_argument("--print", default=False, action="store_true") 108 | parser.add_argument("--ssl", default=False, action="store_true") 109 | parser.add_argument("--buffered", default=False, action="store_true") 110 | args = parser.parse_args() 111 | 112 | if args.uvloop: 113 | import winloop as uvloop 114 | 115 | loop = uvloop.new_event_loop() 116 | print("using UVLoop") 117 | else: 118 | loop = asyncio.new_event_loop() 119 | print("using asyncio loop") 120 | 121 | asyncio.set_event_loop(loop) 122 | loop.set_debug(False) 123 | 124 | if args.print: 125 | PRINT = 1 126 | 127 | if hasattr(loop, "print_debug_info"): 128 | loop.create_task(print_debug(loop)) 129 | PRINT = 0 130 | 131 | unix = False 132 | if args.addr.startswith("file:"): 133 | unix = True 134 | addr = args.addr[5:] 135 | if os.path.exists(addr): 136 | os.remove(addr) 137 | else: 138 | addr = args.addr.split(":") 139 | addr[1] = int(addr[1]) 140 | addr = tuple(addr) 141 | 142 | print("serving on: {}".format(addr)) 143 | 144 | server_context = None 145 | if args.ssl: 146 | print("with SSL") 147 | if hasattr(ssl, "PROTOCOL_TLS"): 148 | server_context = ssl.SSLContext(ssl.PROTOCOL_TLS) 149 | else: 150 | server_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 151 | server_context.load_cert_chain( 152 | ( 153 | pathlib.Path(__file__).parent.parent.parent 154 | / "tests" 155 | / "certs" 156 | / "ssl_cert.pem" 157 | ), 158 | ( 159 | pathlib.Path(__file__).parent.parent.parent 160 | / "tests" 161 | / "certs" 162 | / "ssl_key.pem" 163 | ), 164 | ) 165 | if hasattr(server_context, "check_hostname"): 166 | server_context.check_hostname = False 167 | server_context.verify_mode = ssl.CERT_NONE 168 | 169 | if args.streams: 170 | if args.proto: 171 | print("cannot use --stream and --proto simultaneously") 172 | exit(1) 173 | 174 | if args.buffered: 175 | print("cannot use --stream and --buffered simultaneously") 176 | exit(1) 177 | 178 | print("using asyncio/streams") 179 | if unix: 180 | coro = asyncio.start_unix_server( 181 | echo_client_streams, addr, ssl=server_context 182 | ) 183 | else: 184 | coro = asyncio.start_server(echo_client_streams, *addr, ssl=server_context) 185 | srv = loop.run_until_complete(coro) 186 | elif args.proto: 187 | if args.streams: 188 | print("cannot use --stream and --proto simultaneously") 189 | exit(1) 190 | 191 | if args.buffered: 192 | print("using buffered protocol") 193 | protocol = EchoBufferedProtocol 194 | else: 195 | print("using simple protocol") 196 | protocol = EchoProtocol 197 | 198 | if unix: 199 | coro = loop.create_unix_server(protocol, addr, ssl=server_context) 200 | else: 201 | coro = loop.create_server(protocol, *addr, ssl=server_context) 202 | srv = loop.run_until_complete(coro) 203 | else: 204 | if args.ssl: 205 | print("cannot use SSL for loop.sock_* methods") 206 | exit(1) 207 | 208 | print("using sock_recv/sock_sendall") 209 | loop.create_task(echo_server(loop, addr, unix)) 210 | try: 211 | loop.run_forever() 212 | finally: 213 | if hasattr(loop, "print_debug_info"): 214 | gc.collect() 215 | print(chr(27) + "[2J") 216 | loop.print_debug_info() 217 | 218 | loop.close() 219 | -------------------------------------------------------------------------------- /winloop/loop.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | 4 | from libc.stdint cimport int64_t, uint32_t, uint64_t 5 | 6 | from .includes cimport system, uv 7 | 8 | include "includes/consts.pxi" 9 | 10 | 11 | cdef extern from *: 12 | # TODO: We can get rid of vint soon since volatile is now supported by Cython 13 | ctypedef int vint "volatile int" 14 | 15 | 16 | cdef class UVHandle 17 | cdef class UVSocketHandle(UVHandle) 18 | 19 | cdef class UVAsync(UVHandle) 20 | cdef class UVTimer(UVHandle) 21 | cdef class UVIdle(UVHandle) 22 | 23 | cdef class UVBaseTransport(UVSocketHandle) 24 | 25 | ctypedef object (*method_t)(object) 26 | ctypedef object (*method1_t)(object, object) 27 | ctypedef object (*method2_t)(object, object, object) 28 | ctypedef object (*method3_t)(object, object, object, object) 29 | 30 | 31 | cdef class Loop: 32 | cdef: 33 | uv.uv_loop_t *uvloop 34 | 35 | bint _coroutine_debug_set 36 | int _coroutine_origin_tracking_saved_depth 37 | 38 | public slow_callback_duration 39 | 40 | readonly bint _closed 41 | bint _debug 42 | bint _running 43 | bint _stopping 44 | 45 | uint64_t _thread_id 46 | 47 | object _task_factory 48 | object _exception_handler 49 | object _default_executor 50 | object _ready 51 | set _queued_streams, _executing_streams 52 | Py_ssize_t _ready_len 53 | 54 | set _servers 55 | 56 | object _transports 57 | set _processes 58 | dict _fd_to_reader_fileobj 59 | dict _fd_to_writer_fileobj 60 | dict _unix_server_sockets 61 | 62 | set _signals 63 | dict _signal_handlers 64 | object _ssock 65 | object _csock 66 | bint _listening_signals 67 | int _old_signal_wakeup_id 68 | 69 | set _timers 70 | dict _polls 71 | 72 | UVProcess active_process_handler 73 | 74 | UVAsync handler_async 75 | UVIdle handler_idle 76 | UVCheck handler_check__exec_writes 77 | 78 | object _last_error 79 | 80 | cdef object __weakref__ 81 | 82 | object _asyncgens 83 | bint _asyncgens_shutdown_called 84 | 85 | bint _executor_shutdown_called 86 | 87 | char _recv_buffer[UV_STREAM_RECV_BUF_SIZE] 88 | bint _recv_buffer_in_use 89 | 90 | # DEBUG fields 91 | # True when compiled with DEBUG. 92 | # Used only in unittests. 93 | readonly bint _debug_cc 94 | 95 | readonly object _debug_handles_total 96 | readonly object _debug_handles_closed 97 | readonly object _debug_handles_current 98 | 99 | readonly uint64_t _debug_uv_handles_total 100 | readonly uint64_t _debug_uv_handles_freed 101 | 102 | readonly uint64_t _debug_cb_handles_total 103 | readonly uint64_t _debug_cb_handles_count 104 | readonly uint64_t _debug_cb_timer_handles_total 105 | readonly uint64_t _debug_cb_timer_handles_count 106 | 107 | readonly uint64_t _debug_stream_shutdown_errors_total 108 | readonly uint64_t _debug_stream_listen_errors_total 109 | 110 | readonly uint64_t _debug_stream_read_cb_total 111 | readonly uint64_t _debug_stream_read_cb_errors_total 112 | readonly uint64_t _debug_stream_read_eof_total 113 | readonly uint64_t _debug_stream_read_eof_cb_errors_total 114 | readonly uint64_t _debug_stream_read_errors_total 115 | 116 | readonly uint64_t _debug_stream_write_tries 117 | readonly uint64_t _debug_stream_write_errors_total 118 | readonly uint64_t _debug_stream_write_ctx_total 119 | readonly uint64_t _debug_stream_write_ctx_cnt 120 | readonly uint64_t _debug_stream_write_cb_errors_total 121 | 122 | readonly uint64_t _poll_read_events_total 123 | readonly uint64_t _poll_read_cb_errors_total 124 | readonly uint64_t _poll_write_events_total 125 | readonly uint64_t _poll_write_cb_errors_total 126 | 127 | readonly uint64_t _sock_try_write_total 128 | 129 | readonly uint64_t _debug_exception_handler_cnt 130 | 131 | shlex _shlex_parser 132 | 133 | 134 | cdef _init_debug_fields(self) 135 | 136 | cdef _on_wake(self) 137 | cdef _on_idle(self) 138 | 139 | cdef __run(self, uv.uv_run_mode) 140 | cdef _run(self, uv.uv_run_mode) 141 | 142 | cdef _close(self) 143 | cdef _stop(self, exc) 144 | cdef uint64_t _time(self) 145 | 146 | cdef inline _queue_write(self, UVStream stream) 147 | cdef _exec_queued_writes(self) 148 | 149 | cdef inline Handle _call_soon(self, object callback, object args, object context) 150 | cdef inline _append_ready_handle(self, Handle handle) 151 | cdef inline _call_soon_handle(self, Handle handle) 152 | 153 | cdef TimerHandle _call_later(self, uint64_t delay, object callback, object args, 154 | object context) 155 | 156 | cdef void _handle_exception(self, object ex) 157 | 158 | cdef inline _is_main_thread(self) 159 | 160 | cdef inline _new_future(self) 161 | cdef inline int _check_signal(self, sig) except -1 162 | cdef inline int _check_closed(self) except -1 163 | cdef inline int _check_thread(self) except -1 164 | 165 | cdef _getaddrinfo(self, object host, object port, 166 | int family, int type, 167 | int proto, int flags, 168 | int unpack) 169 | 170 | cdef _getnameinfo(self, system.sockaddr *addr, int flags) 171 | 172 | cdef _track_transport(self, UVBaseTransport transport) 173 | cdef _fileobj_to_fd(self, fileobj) 174 | cdef _ensure_fd_no_transport(self, fd) 175 | 176 | cdef int _track_process(self, UVProcess proc) except -1 177 | cdef int _untrack_process(self, UVProcess proc) except -1 178 | 179 | cdef _add_reader(self, fd, Handle handle) 180 | cdef _has_reader(self, fd) 181 | cdef _remove_reader(self, fd) 182 | 183 | cdef _add_writer(self, fd, Handle handle) 184 | cdef _has_writer(self, fd) 185 | cdef _remove_writer(self, fd) 186 | 187 | cdef _sock_recv(self, fut, sock, n) 188 | cdef _sock_recv_into(self, fut, sock, buf) 189 | cdef _sock_sendall(self, fut, sock, data) 190 | cdef _sock_accept(self, fut, sock) 191 | 192 | cdef _sock_connect(self, sock, address) 193 | cdef _sock_connect_cb(self, fut, sock, address) 194 | 195 | cdef _sock_set_reuseport(self, int fd) 196 | 197 | cdef _setup_or_resume_signals(self) 198 | cdef _shutdown_signals(self) 199 | cdef _pause_signals(self) 200 | 201 | cdef _handle_signal(self, sig) 202 | cdef _read_from_self(self) 203 | cdef inline int _ceval_process_signals(self) except -1 204 | cdef _invoke_signals(self, bytes data) 205 | 206 | cdef _set_coroutine_debug(self, bint enabled) 207 | 208 | cdef _print_debug_info(self) 209 | 210 | 211 | include "cbhandles.pxd" 212 | 213 | include "handles/handle.pxd" 214 | include "handles/async_.pxd" 215 | include "handles/idle.pxd" 216 | include "handles/check.pxd" 217 | include "handles/timer.pxd" 218 | include "handles/poll.pxd" 219 | include "handles/basetransport.pxd" 220 | include "handles/stream.pxd" 221 | include "handles/streamserver.pxd" 222 | include "handles/tcp.pxd" 223 | include "handles/pipe.pxd" 224 | include "handles/process.pxd" 225 | include "handles/fsevent.pxd" 226 | 227 | include "shlex.pxd" 228 | 229 | include "request.pxd" 230 | include "sslproto.pxd" 231 | 232 | include "handles/udp.pxd" 233 | 234 | include "server.pxd" 235 | -------------------------------------------------------------------------------- /winloop/handles/poll.pyx: -------------------------------------------------------------------------------- 1 | @cython.no_gc_clear 2 | cdef class UVPoll(UVHandle): 3 | cdef _init(self, Loop loop, int fd): 4 | cdef int err 5 | 6 | self._start_init(loop) 7 | 8 | self._handle = PyMem_RawMalloc(sizeof(uv.uv_poll_t)) 9 | if self._handle is NULL: 10 | self._abort_init() 11 | raise MemoryError() 12 | 13 | if system.PLATFORM_IS_WINDOWS: 14 | err = uv.uv_poll_init_socket(self._loop.uvloop, 15 | self._handle, fd) 16 | else: 17 | err = uv.uv_poll_init(self._loop.uvloop, 18 | self._handle, fd) 19 | if err < 0: 20 | self._abort_init() 21 | raise convert_error(err) 22 | 23 | self._finish_init() 24 | 25 | self.fd = fd 26 | self.reading_handle = None 27 | self.writing_handle = None 28 | 29 | @staticmethod 30 | cdef UVPoll new(Loop loop, int fd): 31 | cdef UVPoll handle 32 | handle = UVPoll.__new__(UVPoll) 33 | handle._init(loop, fd) 34 | return handle 35 | 36 | cdef int is_active(self) noexcept: 37 | return (self.reading_handle is not None or 38 | self.writing_handle is not None) 39 | 40 | cdef inline _poll_start(self, int flags): 41 | cdef int err 42 | 43 | self._ensure_alive() 44 | 45 | err = uv.uv_poll_start( 46 | self._handle, 47 | flags, 48 | __on_uvpoll_event) 49 | 50 | if err < 0: 51 | exc = convert_error(err) 52 | self._fatal_error(exc, True) 53 | return 54 | 55 | cdef inline _poll_stop(self): 56 | cdef int err 57 | 58 | if not self._is_alive(): 59 | return 60 | 61 | err = uv.uv_poll_stop(self._handle) 62 | if err < 0: 63 | exc = convert_error(err) 64 | self._fatal_error(exc, True) 65 | return 66 | 67 | cdef: 68 | int backend_id 69 | system.epoll_event dummy_event 70 | 71 | if system.PLATFORM_IS_LINUX: 72 | # libuv doesn't remove the FD from epoll immediately 73 | # after uv_poll_stop or uv_poll_close, causing hard 74 | # to debug issue with dup-ed file descriptors causing 75 | # CPU burn in epoll/epoll_ctl: 76 | # https://github.com/MagicStack/uvloop/issues/61 77 | # 78 | # It's safe though to manually call epoll_ctl here, 79 | # after calling uv_poll_stop. 80 | 81 | backend_id = uv.uv_backend_fd(self._loop.uvloop) 82 | if backend_id != -1: 83 | memset(&dummy_event, 0, sizeof(dummy_event)) 84 | system.epoll_ctl( 85 | backend_id, 86 | system.EPOLL_CTL_DEL, 87 | self.fd, 88 | &dummy_event) # ignore errors 89 | 90 | cdef is_reading(self): 91 | return self._is_alive() and self.reading_handle is not None 92 | 93 | cdef is_writing(self): 94 | return self._is_alive() and self.writing_handle is not None 95 | 96 | cdef start_reading(self, Handle callback): 97 | cdef: 98 | int mask = 0 99 | 100 | if self.reading_handle is None: 101 | # not reading right now, setup the handle 102 | 103 | mask = uv.UV_READABLE 104 | if self.writing_handle is not None: 105 | # are we writing right now? 106 | mask |= uv.UV_WRITABLE 107 | 108 | self._poll_start(mask) 109 | else: 110 | self.reading_handle._cancel() 111 | 112 | self.reading_handle = callback 113 | 114 | cdef start_writing(self, Handle callback): 115 | cdef: 116 | int mask = 0 117 | 118 | if self.writing_handle is None: 119 | # not writing right now, setup the handle 120 | 121 | mask = uv.UV_WRITABLE 122 | if self.reading_handle is not None: 123 | # are we reading right now? 124 | mask |= uv.UV_READABLE 125 | 126 | self._poll_start(mask) 127 | else: 128 | self.writing_handle._cancel() 129 | 130 | self.writing_handle = callback 131 | 132 | cdef stop_reading(self): 133 | if self.reading_handle is None: 134 | return False 135 | 136 | self.reading_handle._cancel() 137 | self.reading_handle = None 138 | 139 | if self.writing_handle is None: 140 | self.stop() 141 | else: 142 | self._poll_start(uv.UV_WRITABLE) 143 | 144 | return True 145 | 146 | cdef stop_writing(self): 147 | if self.writing_handle is None: 148 | return False 149 | 150 | self.writing_handle._cancel() 151 | self.writing_handle = None 152 | 153 | if self.reading_handle is None: 154 | self.stop() 155 | else: 156 | self._poll_start(uv.UV_READABLE) 157 | 158 | return True 159 | 160 | cdef stop(self): 161 | if self.reading_handle is not None: 162 | self.reading_handle._cancel() 163 | self.reading_handle = None 164 | 165 | if self.writing_handle is not None: 166 | self.writing_handle._cancel() 167 | self.writing_handle = None 168 | 169 | self._poll_stop() 170 | 171 | cdef _close(self): 172 | if self.is_active(): 173 | self.stop() 174 | 175 | UVHandle._close(self) 176 | 177 | cdef _fatal_error(self, exc, throw, reason=None): 178 | try: 179 | if self.reading_handle is not None: 180 | try: 181 | self.reading_handle._run() 182 | except BaseException as ex: 183 | self._loop._handle_exception(ex) 184 | self.reading_handle = None 185 | 186 | if self.writing_handle is not None: 187 | try: 188 | self.writing_handle._run() 189 | except BaseException as ex: 190 | self._loop._handle_exception(ex) 191 | self.writing_handle = None 192 | 193 | finally: 194 | self._close() 195 | 196 | 197 | cdef void __on_uvpoll_event( 198 | uv.uv_poll_t* handle, 199 | int status, 200 | int events, 201 | ) noexcept with gil: 202 | 203 | if __ensure_handle_data(handle, "UVPoll callback") == 0: 204 | return 205 | 206 | cdef: 207 | UVPoll poll = handle.data 208 | 209 | if status < 0: 210 | exc = convert_error(status) 211 | poll._fatal_error(exc, False) 212 | return 213 | 214 | if ((events & (uv.UV_READABLE | uv.UV_DISCONNECT)) and 215 | poll.reading_handle is not None): 216 | 217 | try: 218 | if UVLOOP_DEBUG: 219 | poll._loop._poll_read_events_total += 1 220 | poll.reading_handle._run() 221 | except BaseException as ex: 222 | if UVLOOP_DEBUG: 223 | poll._loop._poll_read_cb_errors_total += 1 224 | poll._error(ex, False) 225 | # continue code execution 226 | 227 | if ((events & (uv.UV_WRITABLE | uv.UV_DISCONNECT)) and 228 | poll.writing_handle is not None): 229 | 230 | try: 231 | if UVLOOP_DEBUG: 232 | poll._loop._poll_write_events_total += 1 233 | poll.writing_handle._run() 234 | except BaseException as ex: 235 | if UVLOOP_DEBUG: 236 | poll._loop._poll_write_cb_errors_total += 1 237 | poll._error(ex, False) 238 | -------------------------------------------------------------------------------- /winloop/includes/stdlib.pxi: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | 4 | import asyncio, asyncio.log, asyncio.base_events, \ 5 | asyncio.sslproto, asyncio.coroutines, \ 6 | asyncio.futures, asyncio.transports 7 | import collections.abc 8 | import concurrent.futures 9 | import errno 10 | import functools 11 | import gc 12 | import inspect 13 | import itertools 14 | import os 15 | import signal 16 | import socket 17 | import subprocess 18 | import ssl 19 | import stat 20 | import sys 21 | import threading 22 | import traceback 23 | import time 24 | import warnings 25 | import weakref 26 | 27 | 28 | from cpython.object cimport PyObject, PyTypeObject 29 | from cpython.time cimport PyTime_AsSecondsDouble, PyTime_t 30 | from libc.stdint cimport uintptr_t 31 | 32 | 33 | # TODO: Request or Propose to CPython Maintainers to allow public use of many of these functions via Capsule 34 | # for improved performance. 35 | 36 | cdef aio_get_event_loop = asyncio.get_event_loop 37 | cdef aio_CancelledError = asyncio.CancelledError 38 | cdef aio_InvalidStateError = asyncio.InvalidStateError 39 | cdef aio_TimeoutError = asyncio.TimeoutError 40 | cdef aio_Future = asyncio.Future 41 | cdef aio_Task = asyncio.Task 42 | cdef aio_ensure_future = asyncio.ensure_future 43 | cdef aio_gather = asyncio.gather 44 | cdef aio_wait = asyncio.wait 45 | cdef aio_wrap_future = asyncio.wrap_future 46 | cdef aio_logger = asyncio.log.logger 47 | cdef aio_iscoroutine = asyncio.iscoroutine 48 | cdef aio_iscoroutinefunction = asyncio.iscoroutinefunction 49 | cdef aio_BaseProtocol = asyncio.BaseProtocol 50 | 51 | cdef aio_Protocol = asyncio.Protocol 52 | cdef aio_isfuture = getattr(asyncio, 'isfuture', None) 53 | cdef aio_get_running_loop = getattr(asyncio, '_get_running_loop', None) 54 | cdef aio_set_running_loop = getattr(asyncio, '_set_running_loop', None) 55 | cdef aio_debug_wrapper = getattr(asyncio.coroutines, 'debug_wrapper', None) 56 | cdef aio_AbstractChildWatcher = getattr(asyncio, 'AbstractChildWatcher', None) 57 | cdef aio_Transport = asyncio.Transport 58 | cdef aio_FlowControlMixin = asyncio.transports._FlowControlMixin 59 | 60 | cdef col_deque = collections.deque 61 | cdef col_Iterable = collections.abc.Iterable 62 | cdef col_Counter = collections.Counter 63 | 64 | # cdef col_OrderedDict = collections.OrderedDict 65 | 66 | cdef cc_ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor 67 | cdef cc_Future = concurrent.futures.Future 68 | 69 | cdef errno_EBADF = errno.EBADF 70 | cdef errno_EINVAL = errno.EINVAL 71 | 72 | # TODO: Maybe we should try hacking in the partial code into cython instead? 73 | cdef ft_partial = functools.partial 74 | 75 | cdef gc_disable = gc.disable 76 | 77 | cdef iter_chain = itertools.chain 78 | cdef inspect_isgenerator = inspect.isgenerator 79 | 80 | cdef int has_IPV6_V6ONLY = hasattr(socket, 'IPV6_V6ONLY') 81 | cdef int IPV6_V6ONLY = getattr(socket, 'IPV6_V6ONLY', -1) 82 | cdef int has_SO_REUSEPORT = hasattr(socket, 'SO_REUSEPORT') 83 | cdef int SO_REUSEPORT = getattr(socket, 'SO_REUSEPORT', 0) 84 | cdef int SO_BROADCAST = getattr(socket, 'SO_BROADCAST') 85 | cdef int SOCK_NONBLOCK = getattr(socket, 'SOCK_NONBLOCK', -1) 86 | cdef int socket_AI_CANONNAME = getattr(socket, 'AI_CANONNAME') 87 | 88 | 89 | # NOTE: Recently Managed to hack these in with CPython's _socket.CAPI Capsule they are left in the code. 90 | # it's avalible on all versions of python currently so we may move to using it soon. 91 | # SEE: https://gist.github.com/Vizonex/d24b8d4c22027449b3ec175583a93aea 92 | # WARNING: Idea is still theorical and not ready! 93 | 94 | 95 | # it is very likely that an array.array utility hack will force it in correctly 96 | # doing so will make any of these functions run faster & smoother. 97 | # (Mainly typechecks and returntypes only) 98 | 99 | 100 | cdef socket_gaierror = socket.gaierror 101 | cdef socket_error = socket.error 102 | cdef socket_timeout = socket.timeout 103 | cdef socket_socket = socket.socket 104 | cdef socket_socketpair = socket.socketpair 105 | cdef socket_getservbyname = socket.getservbyname 106 | cdef socket_AddressFamily = socket.AddressFamily 107 | cdef socket_SocketKind = socket.SocketKind 108 | 109 | cdef int socket_EAI_ADDRFAMILY = getattr(socket, 'EAI_ADDRFAMILY', -1) 110 | cdef int socket_EAI_AGAIN = getattr(socket, 'EAI_AGAIN', -1) 111 | cdef int socket_EAI_BADFLAGS = getattr(socket, 'EAI_BADFLAGS', -1) 112 | cdef int socket_EAI_BADHINTS = getattr(socket, 'EAI_BADHINTS', -1) 113 | cdef int socket_EAI_CANCELED = getattr(socket, 'EAI_CANCELED', -1) 114 | cdef int socket_EAI_FAIL = getattr(socket, 'EAI_FAIL', -1) 115 | cdef int socket_EAI_FAMILY = getattr(socket, 'EAI_FAMILY', -1) 116 | cdef int socket_EAI_MEMORY = getattr(socket, 'EAI_MEMORY', -1) 117 | cdef int socket_EAI_NODATA = getattr(socket, 'EAI_NODATA', -1) 118 | cdef int socket_EAI_NONAME = getattr(socket, 'EAI_NONAME', -1) 119 | cdef int socket_EAI_OVERFLOW = getattr(socket, 'EAI_OVERFLOW', -1) 120 | cdef int socket_EAI_PROTOCOL = getattr(socket, 'EAI_PROTOCOL', -1) 121 | cdef int socket_EAI_SERVICE = getattr(socket, 'EAI_SERVICE', -1) 122 | cdef int socket_EAI_SOCKTYPE = getattr(socket, 'EAI_SOCKTYPE', -1) 123 | 124 | 125 | cdef str os_name = os.name 126 | cdef os_environ = os.environ 127 | cdef os_dup = os.dup 128 | cdef os_set_inheritable = os.set_inheritable 129 | cdef os_get_inheritable = os.get_inheritable 130 | cdef os_close = os.close 131 | cdef os_open = os.open 132 | cdef os_devnull = os.devnull 133 | cdef os_O_RDWR = os.O_RDWR 134 | cdef os_pipe = os.pipe 135 | cdef os_read = os.read 136 | cdef os_remove = os.remove 137 | cdef os_stat = os.stat 138 | cdef os_unlink = os.unlink 139 | cdef os_fspath = os.fspath 140 | 141 | cdef stat_S_ISSOCK = stat.S_ISSOCK 142 | 143 | cdef sys_ignore_environment = sys.flags.ignore_environment 144 | cdef sys_dev_mode = sys.flags.dev_mode 145 | cdef sys_exc_info = sys.exc_info 146 | cdef sys_set_coroutine_wrapper = getattr(sys, 'set_coroutine_wrapper', None) 147 | cdef sys_get_coroutine_wrapper = getattr(sys, 'get_coroutine_wrapper', None) 148 | cdef sys_getframe = sys._getframe 149 | cdef sys_version_info = sys.version_info 150 | cdef sys_getfilesystemencoding = sys.getfilesystemencoding 151 | cdef str sys_platform = sys.platform 152 | 153 | cdef ssl_SSLContext = ssl.SSLContext 154 | cdef ssl_MemoryBIO = ssl.MemoryBIO 155 | cdef ssl_create_default_context = ssl.create_default_context 156 | cdef ssl_SSLError = ssl.SSLError 157 | cdef ssl_SSLAgainErrors = (ssl.SSLWantReadError, ssl.SSLSyscallError) 158 | cdef ssl_SSLZeroReturnError = ssl.SSLZeroReturnError 159 | cdef ssl_CertificateError = ssl.CertificateError 160 | cdef int ssl_SSL_ERROR_WANT_READ = ssl.SSL_ERROR_WANT_READ 161 | cdef int ssl_SSL_ERROR_WANT_WRITE = ssl.SSL_ERROR_WANT_WRITE 162 | cdef int ssl_SSL_ERROR_SYSCALL = ssl.SSL_ERROR_SYSCALL 163 | 164 | cdef threading_Thread = threading.Thread 165 | cdef threading_main_thread = threading.main_thread 166 | 167 | cdef int subprocess_PIPE = subprocess.PIPE 168 | cdef int subprocess_STDOUT = subprocess.STDOUT 169 | cdef int subprocess_DEVNULL = subprocess.DEVNULL 170 | cdef subprocess_SubprocessError = subprocess.SubprocessError 171 | 172 | cdef int signal_NSIG = signal.NSIG 173 | cdef signal_signal = signal.signal 174 | cdef signal_siginterrupt = getattr(signal, 'siginterrupt', None) 175 | # "I'll use SIGABRT Unless some other developer finds problems with this" - Vizonex 176 | cdef signal_SIGABRT = signal.SIGABRT 177 | cdef signal_SIGINT = signal.SIGINT 178 | cdef signal_set_wakeup_fd = signal.set_wakeup_fd 179 | cdef signal_default_int_handler = signal.default_int_handler 180 | cdef signal_SIG_DFL = signal.SIG_DFL 181 | 182 | cdef time_sleep = time.sleep 183 | cdef time_monotonic = time.monotonic 184 | 185 | cdef tb_StackSummary = traceback.StackSummary 186 | cdef tb_walk_stack = traceback.walk_stack 187 | cdef tb_format_list = traceback.format_list 188 | 189 | cdef warnings_warn = warnings.warn 190 | 191 | cdef weakref_WeakValueDictionary = weakref.WeakValueDictionary 192 | cdef weakref_WeakSet = weakref.WeakSet 193 | 194 | cdef py_inf = float('inf') 195 | 196 | 197 | # Cython doesn't clean-up imported objects properly in Py3 mode, 198 | # so we delete refs to all modules manually (except sys and errno) 199 | del asyncio, concurrent, collections 200 | del functools, inspect, itertools, socket, os, threading 201 | del signal, subprocess, ssl 202 | del time, traceback, warnings, weakref 203 | -------------------------------------------------------------------------------- /winloop/handles/tcp.pyx: -------------------------------------------------------------------------------- 1 | cdef __tcp_init_uv_handle(UVStream handle, Loop loop, unsigned int flags): 2 | cdef int err 3 | 4 | handle._handle = PyMem_RawMalloc(sizeof(uv.uv_tcp_t)) 5 | if handle._handle is NULL: 6 | handle._abort_init() 7 | raise MemoryError() 8 | 9 | err = uv.uv_tcp_init_ex(handle._loop.uvloop, 10 | handle._handle, 11 | flags) 12 | if err < 0: 13 | handle._abort_init() 14 | raise convert_error(err) 15 | 16 | handle._finish_init() 17 | 18 | 19 | cdef __tcp_bind(UVStream handle, system.sockaddr* addr, unsigned int flags): 20 | cdef int err 21 | err = uv.uv_tcp_bind(handle._handle, 22 | addr, flags) 23 | if err < 0: 24 | exc = convert_error(err) 25 | raise exc 26 | 27 | 28 | cdef __tcp_open(UVStream handle, int sockfd): 29 | cdef int err 30 | err = uv.uv_tcp_open(handle._handle, 31 | sockfd) 32 | if err < 0: 33 | exc = convert_error(err) 34 | raise exc 35 | 36 | 37 | cdef __tcp_get_socket(UVSocketHandle handle): 38 | cdef: 39 | int buf_len = sizeof(system.sockaddr_storage) 40 | int fileno 41 | int err 42 | system.sockaddr_storage buf 43 | 44 | fileno = handle._fileno() 45 | 46 | err = uv.uv_tcp_getsockname(handle._handle, 47 | &buf, 48 | &buf_len) 49 | if err < 0: 50 | raise convert_error(err) 51 | 52 | return PseudoSocket(buf.ss_family, uv.SOCK_STREAM, 0, fileno) 53 | 54 | 55 | @cython.no_gc_clear 56 | cdef class TCPServer(UVStreamServer): 57 | 58 | @staticmethod 59 | cdef TCPServer new(Loop loop, object protocol_factory, Server server, 60 | unsigned int flags, 61 | object backlog, 62 | object ssl, 63 | object ssl_handshake_timeout, 64 | object ssl_shutdown_timeout): 65 | 66 | cdef TCPServer handle 67 | handle = TCPServer.__new__(TCPServer) 68 | handle._init(loop, protocol_factory, server, backlog, 69 | ssl, ssl_handshake_timeout, ssl_shutdown_timeout) 70 | __tcp_init_uv_handle(handle, loop, flags) 71 | return handle 72 | 73 | cdef _new_socket(self): 74 | return __tcp_get_socket(self) 75 | 76 | cdef _open(self, int sockfd): 77 | self._ensure_alive() 78 | try: 79 | __tcp_open(self, sockfd) 80 | except Exception as exc: 81 | self._fatal_error(exc, True) 82 | else: 83 | self._mark_as_open() 84 | 85 | cdef bind(self, system.sockaddr* addr, unsigned int flags=0): 86 | self._ensure_alive() 87 | try: 88 | __tcp_bind(self, addr, flags) 89 | except Exception as exc: 90 | self._fatal_error(exc, True) 91 | else: 92 | self._mark_as_open() 93 | 94 | cdef UVStream _make_new_transport(self, object protocol, object waiter, 95 | object context): 96 | cdef TCPTransport tr 97 | tr = TCPTransport.new(self._loop, protocol, self._server, waiter, 98 | context) 99 | return tr 100 | 101 | 102 | @cython.no_gc_clear 103 | cdef class TCPTransport(UVStream): 104 | 105 | @staticmethod 106 | cdef TCPTransport new(Loop loop, object protocol, Server server, 107 | object waiter, object context): 108 | 109 | cdef TCPTransport handle 110 | handle = TCPTransport.__new__(TCPTransport) 111 | handle._init(loop, protocol, server, waiter, context) 112 | __tcp_init_uv_handle(handle, loop, uv.AF_UNSPEC) 113 | handle.__peername_set = 0 114 | handle.__sockname_set = 0 115 | handle._set_nodelay() 116 | return handle 117 | 118 | cdef _set_nodelay(self): 119 | cdef int err 120 | self._ensure_alive() 121 | err = uv.uv_tcp_nodelay(self._handle, 1) 122 | if err < 0: 123 | raise convert_error(err) 124 | 125 | cdef _call_connection_made(self): 126 | # asyncio saves peername & sockname when transports are instantiated, 127 | # so that they're accessible even after the transport is closed. 128 | # We are doing the same thing here, except that we create Python 129 | # objects lazily, on request in get_extra_info() 130 | 131 | cdef: 132 | int err 133 | int buf_len 134 | 135 | buf_len = sizeof(system.sockaddr_storage) 136 | err = uv.uv_tcp_getsockname(self._handle, 137 | &self.__sockname, 138 | &buf_len) 139 | if err >= 0: 140 | # Ignore errors, this is an optional thing. 141 | # If something serious is going on, the transport 142 | # will crash later (in roughly the same way how 143 | # an asyncio transport would.) 144 | self.__sockname_set = 1 145 | 146 | buf_len = sizeof(system.sockaddr_storage) 147 | err = uv.uv_tcp_getpeername(self._handle, 148 | &self.__peername, 149 | &buf_len) 150 | if err >= 0: 151 | # Same as few lines above -- we don't really care 152 | # about error case here. 153 | self.__peername_set = 1 154 | 155 | UVBaseTransport._call_connection_made(self) 156 | 157 | def get_extra_info(self, name, default=None): 158 | if name == 'sockname': 159 | if self.__sockname_set: 160 | return __convert_sockaddr_to_pyaddr( 161 | &self.__sockname) 162 | elif name == 'peername': 163 | if self.__peername_set: 164 | return __convert_sockaddr_to_pyaddr( 165 | &self.__peername) 166 | return super().get_extra_info(name, default) 167 | 168 | cdef _new_socket(self): 169 | return __tcp_get_socket(self) 170 | 171 | cdef bind(self, system.sockaddr* addr, unsigned int flags=0): 172 | self._ensure_alive() 173 | __tcp_bind(self, addr, flags) 174 | 175 | cdef _open(self, int sockfd): 176 | self._ensure_alive() 177 | __tcp_open(self, sockfd) 178 | 179 | cdef connect(self, system.sockaddr* addr): 180 | cdef _TCPConnectRequest req 181 | req = _TCPConnectRequest(self._loop, self) 182 | req.connect(addr) 183 | 184 | 185 | cdef class _TCPConnectRequest(UVRequest): 186 | cdef: 187 | TCPTransport transport 188 | uv.uv_connect_t _req_data 189 | 190 | def __cinit__(self, loop, transport): 191 | self.request = &self._req_data 192 | self.request.data = self 193 | self.transport = transport 194 | 195 | cdef connect(self, system.sockaddr* addr): 196 | cdef int err 197 | err = uv.uv_tcp_connect(self.request, 198 | self.transport._handle, 199 | addr, 200 | __tcp_connect_callback) 201 | if err < 0: 202 | exc = convert_error(err) 203 | self.on_done() 204 | raise exc 205 | 206 | 207 | cdef void __tcp_connect_callback( 208 | uv.uv_connect_t* req, 209 | int status, 210 | ) noexcept with gil: 211 | cdef: 212 | _TCPConnectRequest wrapper 213 | TCPTransport transport 214 | 215 | wrapper = <_TCPConnectRequest> req.data 216 | transport = wrapper.transport 217 | 218 | if status < 0: 219 | exc = convert_error(status) 220 | else: 221 | exc = None 222 | 223 | try: 224 | transport._on_connect(exc) 225 | except BaseException as ex: 226 | wrapper.transport._fatal_error(ex, False) 227 | finally: 228 | wrapper.on_done() 229 | -------------------------------------------------------------------------------- /winloop/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio as __asyncio 2 | import collections.abc as _collections_abc 3 | import typing as _typing 4 | import sys as _sys 5 | import warnings as _warnings 6 | 7 | from . import includes as __includes # NOQA 8 | from .loop import Loop as __BaseLoop # NOQA 9 | from ._version import __version__ # NOQA 10 | 11 | 12 | __all__: tuple[str, ...] = ("new_event_loop", "run") 13 | _AbstractEventLoop = __asyncio.AbstractEventLoop 14 | 15 | 16 | _T = _typing.TypeVar("_T") 17 | 18 | 19 | class Loop(__BaseLoop, _AbstractEventLoop): # type: ignore[misc] 20 | pass 21 | 22 | 23 | def new_event_loop() -> Loop: 24 | """Return a new event loop.""" 25 | return Loop() 26 | 27 | 28 | if _typing.TYPE_CHECKING: 29 | 30 | def run( 31 | main: _collections_abc.Coroutine[_typing.Any, _typing.Any, _T], 32 | *, 33 | loop_factory: _collections_abc.Callable[[], Loop] | None = new_event_loop, 34 | debug: bool | None = None, 35 | ) -> _T: 36 | """The preferred way of running a coroutine with winloop.""" 37 | else: 38 | 39 | def run(main, *, loop_factory=new_event_loop, debug=None, **run_kwargs): 40 | """The preferred way of running a coroutine with winloop.""" 41 | 42 | async def wrapper(): 43 | # If `loop_factory` is provided we want it to return 44 | # either winloop.Loop or a subtype of it, assuming the user 45 | # is using `winloop.run()` intentionally. 46 | loop = __asyncio._get_running_loop() 47 | if not isinstance(loop, Loop): 48 | raise TypeError("winloop.run() uses a non-winloop event loop") 49 | return await main 50 | 51 | vi = _sys.version_info[:2] 52 | 53 | if vi <= (3, 10): 54 | # Copied from python/cpython 55 | 56 | if __asyncio._get_running_loop() is not None: 57 | raise RuntimeError( 58 | "asyncio.run() cannot be called from a running event loop" 59 | ) 60 | 61 | if not __asyncio.iscoroutine(main): 62 | raise ValueError("a coroutine was expected, got {!r}".format(main)) 63 | 64 | loop = loop_factory() 65 | try: 66 | __asyncio.set_event_loop(loop) 67 | if debug is not None: 68 | loop.set_debug(debug) 69 | return loop.run_until_complete(wrapper()) 70 | finally: 71 | try: 72 | _cancel_all_tasks(loop) 73 | loop.run_until_complete(loop.shutdown_asyncgens()) 74 | if hasattr(loop, "shutdown_default_executor"): 75 | loop.run_until_complete(loop.shutdown_default_executor()) 76 | finally: 77 | __asyncio.set_event_loop(None) 78 | loop.close() 79 | 80 | elif vi == (3, 11): 81 | if __asyncio._get_running_loop() is not None: 82 | raise RuntimeError( 83 | "asyncio.run() cannot be called from a running event loop" 84 | ) 85 | 86 | with __asyncio.Runner( 87 | loop_factory=loop_factory, debug=debug, **run_kwargs 88 | ) as runner: 89 | return runner.run(wrapper()) 90 | 91 | else: 92 | assert vi >= (3, 12) 93 | return __asyncio.run( 94 | wrapper(), loop_factory=loop_factory, debug=debug, **run_kwargs 95 | ) 96 | 97 | 98 | def _cancel_all_tasks(loop: _AbstractEventLoop) -> None: 99 | # Copied from python/cpython 100 | 101 | to_cancel = __asyncio.all_tasks(loop) 102 | if not to_cancel: 103 | return 104 | 105 | for task in to_cancel: 106 | task.cancel() 107 | 108 | loop.run_until_complete(__asyncio.gather(*to_cancel, return_exceptions=True)) 109 | 110 | for task in to_cancel: 111 | if task.cancelled(): 112 | continue 113 | if task.exception() is not None: 114 | loop.call_exception_handler( 115 | { 116 | "message": "unhandled exception during asyncio.run() shutdown", 117 | "exception": task.exception(), 118 | "task": task, 119 | } 120 | ) 121 | 122 | 123 | _deprecated_names = ("install", "EventLoopPolicy") 124 | 125 | 126 | if _sys.version_info[:2] < (3, 16): 127 | __all__ += _deprecated_names 128 | 129 | 130 | def __getattr__(name: str) -> _typing.Any: 131 | if name not in _deprecated_names: 132 | raise AttributeError(f"module 'winloop' has no attribute '{name}'") 133 | elif _sys.version_info[:2] >= (3, 16): 134 | raise AttributeError( 135 | f"module 'winloop' has no attribute '{name}' " 136 | f"(it was removed in Python 3.16, use winloop.run() instead)" 137 | ) 138 | 139 | import threading 140 | 141 | def install() -> None: 142 | """A helper function to install winloop policy. 143 | 144 | This function is deprecated and will be removed in Python 3.16. 145 | Use `winloop.run()` instead. 146 | """ 147 | if _sys.version_info[:2] >= (3, 12): 148 | _warnings.warn( 149 | "winloop.install() is deprecated in favor of winloop.run() " 150 | "starting with Python 3.12.", 151 | DeprecationWarning, 152 | stacklevel=1, 153 | ) 154 | __asyncio.set_event_loop_policy(EventLoopPolicy()) 155 | 156 | class EventLoopPolicy( 157 | # This is to avoid a mypy error about AbstractEventLoopPolicy 158 | getattr(__asyncio, "AbstractEventLoopPolicy") # type: ignore[misc] 159 | ): 160 | """Event loop policy for winloop. 161 | 162 | This class is deprecated and will be removed in Python 3.16. 163 | Use `winloop.run()` instead. 164 | 165 | >>> import asyncio 166 | >>> import winloop 167 | >>> asyncio.set_event_loop_policy(winloop.EventLoopPolicy()) 168 | >>> asyncio.get_event_loop() 169 | 170 | """ 171 | 172 | def _loop_factory(self) -> Loop: 173 | return new_event_loop() 174 | 175 | if _typing.TYPE_CHECKING: 176 | # EventLoopPolicy doesn't implement these, but since they are 177 | # marked as abstract in typeshed, we have to put them in so mypy 178 | # thinks the base methods are overridden. This is the same approach 179 | # taken for the Windows event loop policy classes in typeshed. 180 | def get_child_watcher(self) -> _typing.NoReturn: ... 181 | 182 | def set_child_watcher(self, watcher: _typing.Any) -> _typing.NoReturn: ... 183 | 184 | class _Local(threading.local): 185 | _loop: _AbstractEventLoop | None = None 186 | 187 | def __init__(self) -> None: 188 | self._local = self._Local() 189 | 190 | def get_event_loop(self) -> _AbstractEventLoop: 191 | """Get the event loop for the current context. 192 | 193 | Returns an instance of EventLoop or raises an exception. 194 | """ 195 | if self._local._loop is None: 196 | raise RuntimeError( 197 | "There is no current event loop in thread %r." 198 | % threading.current_thread().name 199 | ) 200 | 201 | return self._local._loop 202 | 203 | def set_event_loop(self, loop: _AbstractEventLoop | None) -> None: 204 | """Set the event loop.""" 205 | if loop is not None and not isinstance(loop, _AbstractEventLoop): 206 | raise TypeError( 207 | f"loop must be an instance of AbstractEventLoop or None, " 208 | f"not '{type(loop).__name__}'" 209 | ) 210 | self._local._loop = loop 211 | 212 | def new_event_loop(self) -> Loop: 213 | """Create a new event loop. 214 | 215 | You must call set_event_loop() to make this the current event loop. 216 | """ 217 | return self._loop_factory() 218 | 219 | globals()["install"] = install 220 | globals()["EventLoopPolicy"] = EventLoopPolicy 221 | return globals()[name] 222 | -------------------------------------------------------------------------------- /winloop/handles/pipe.pyx: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uintptr_t 2 | 3 | cdef __pipe_init_uv_handle(UVStream handle, Loop loop): 4 | cdef int err 5 | 6 | handle._handle = PyMem_RawMalloc(sizeof(uv.uv_pipe_t)) 7 | if handle._handle is NULL: 8 | handle._abort_init() 9 | raise MemoryError() 10 | 11 | # Initialize pipe handle with ipc=0. 12 | # ipc=1 means that libuv will use recvmsg/sendmsg 13 | # instead of recv/send. 14 | err = uv.uv_pipe_init(handle._loop.uvloop, 15 | handle._handle, 16 | 0) 17 | # UV_HANDLE_READABLE allows calling uv_read_start() on this pipe 18 | # even if it is O_WRONLY, see also #317, libuv/libuv#2058 19 | handle._handle.flags |= uv.UV_INTERNAL_HANDLE_READABLE 20 | if err < 0: 21 | handle._abort_init() 22 | raise convert_error(err) 23 | 24 | handle._finish_init() 25 | 26 | 27 | cdef __pipe_open(UVStream handle, int fd): 28 | cdef int err 29 | err = uv.uv_pipe_open(handle._handle, 30 | fd) 31 | if err < 0: 32 | exc = convert_error(err) 33 | raise exc 34 | 35 | 36 | cdef __pipe_get_socket(UVSocketHandle handle): 37 | fileno = handle._fileno() 38 | return PseudoSocket(uv.AF_UNIX, uv.SOCK_STREAM, 0, fileno) 39 | 40 | 41 | @cython.no_gc_clear 42 | cdef class UnixServer(UVStreamServer): 43 | 44 | @staticmethod 45 | cdef UnixServer new(Loop loop, object protocol_factory, Server server, 46 | object backlog, 47 | object ssl, 48 | object ssl_handshake_timeout, 49 | object ssl_shutdown_timeout): 50 | 51 | cdef UnixServer handle 52 | handle = UnixServer.__new__(UnixServer) 53 | handle._init(loop, protocol_factory, server, backlog, 54 | ssl, ssl_handshake_timeout, ssl_shutdown_timeout) 55 | __pipe_init_uv_handle(handle, loop) 56 | return handle 57 | 58 | cdef _new_socket(self): 59 | return __pipe_get_socket(self) 60 | 61 | cdef _open(self, int sockfd): 62 | self._ensure_alive() 63 | __pipe_open(self, sockfd) 64 | self._mark_as_open() 65 | 66 | cdef bind(self, str path): 67 | cdef int err 68 | self._ensure_alive() 69 | err = uv.uv_pipe_bind(self._handle, 70 | path.encode()) 71 | if err < 0: 72 | exc = convert_error(err) 73 | self._fatal_error(exc, True) 74 | return 75 | 76 | self._mark_as_open() 77 | 78 | cdef UVStream _make_new_transport(self, object protocol, object waiter, 79 | object context): 80 | cdef UnixTransport tr 81 | tr = UnixTransport.new(self._loop, protocol, self._server, waiter, 82 | context) 83 | return tr 84 | 85 | cdef _close(self): 86 | sock = self._fileobj 87 | if sock is not None and sock in self._loop._unix_server_sockets: 88 | path = sock.getsockname() 89 | else: 90 | path = None 91 | 92 | UVStreamServer._close(self) 93 | 94 | if path is not None: 95 | prev_ino = self._loop._unix_server_sockets[sock] 96 | del self._loop._unix_server_sockets[sock] 97 | try: 98 | if os_stat(path).st_ino == prev_ino: 99 | os_unlink(path) 100 | except FileNotFoundError: 101 | pass 102 | except OSError as err: 103 | aio_logger.error('Unable to clean up listening UNIX socket ' 104 | '%r: %r', path, err) 105 | 106 | 107 | @cython.no_gc_clear 108 | cdef class UnixTransport(UVStream): 109 | 110 | @staticmethod 111 | cdef UnixTransport new(Loop loop, object protocol, Server server, 112 | object waiter, object context): 113 | 114 | cdef UnixTransport handle 115 | handle = UnixTransport.__new__(UnixTransport) 116 | handle._init(loop, protocol, server, waiter, context) 117 | __pipe_init_uv_handle(handle, loop) 118 | return handle 119 | 120 | cdef _new_socket(self): 121 | return __pipe_get_socket(self) 122 | 123 | cdef _open(self, int sockfd): 124 | __pipe_open(self, sockfd) 125 | 126 | cdef connect(self, char* addr): 127 | cdef _PipeConnectRequest req 128 | req = _PipeConnectRequest(self._loop, self) 129 | req.connect(addr) 130 | 131 | 132 | @cython.no_gc_clear 133 | cdef class ReadUnixTransport(UVStream): 134 | 135 | @staticmethod 136 | cdef ReadUnixTransport new(Loop loop, object protocol, Server server, 137 | object waiter): 138 | cdef ReadUnixTransport handle 139 | handle = ReadUnixTransport.__new__(ReadUnixTransport) 140 | # This is only used in connect_read_pipe() and subprocess_shell/exec() 141 | # directly, we could simply copy the current context. 142 | handle._init(loop, protocol, server, waiter, Context_CopyCurrent()) 143 | __pipe_init_uv_handle(handle, loop) 144 | return handle 145 | 146 | cdef _new_socket(self): 147 | return __pipe_get_socket(self) 148 | 149 | cdef _open(self, int sockfd): 150 | __pipe_open(self, sockfd) 151 | 152 | def get_write_buffer_limits(self): 153 | raise NotImplementedError 154 | 155 | def set_write_buffer_limits(self, high=None, low=None): 156 | raise NotImplementedError 157 | 158 | def get_write_buffer_size(self): 159 | raise NotImplementedError 160 | 161 | def write(self, data): 162 | raise NotImplementedError 163 | 164 | def writelines(self, list_of_data): 165 | raise NotImplementedError 166 | 167 | def write_eof(self): 168 | raise NotImplementedError 169 | 170 | def can_write_eof(self): 171 | raise NotImplementedError 172 | 173 | def abort(self): 174 | raise NotImplementedError 175 | 176 | 177 | @cython.no_gc_clear 178 | cdef class WriteUnixTransport(UVStream): 179 | 180 | @staticmethod 181 | cdef WriteUnixTransport new(Loop loop, object protocol, Server server, 182 | object waiter): 183 | cdef WriteUnixTransport handle 184 | handle = WriteUnixTransport.__new__(WriteUnixTransport) 185 | 186 | # We listen for read events on write-end of the pipe. When 187 | # the read-end is close, the uv_stream_t.read callback will 188 | # receive an error -- we want to silence that error, and just 189 | # close the transport. 190 | handle._close_on_read_error() 191 | 192 | # This is only used in connect_write_pipe() and subprocess_shell/exec() 193 | # directly, we could simply copy the current context. 194 | handle._init(loop, protocol, server, waiter, Context_CopyCurrent()) 195 | __pipe_init_uv_handle(handle, loop) 196 | return handle 197 | 198 | cdef _new_socket(self): 199 | return __pipe_get_socket(self) 200 | 201 | cdef _open(self, int sockfd): 202 | __pipe_open(self, sockfd) 203 | 204 | def pause_reading(self): 205 | raise NotImplementedError 206 | 207 | def resume_reading(self): 208 | raise NotImplementedError 209 | 210 | 211 | cdef class _PipeConnectRequest(UVRequest): 212 | cdef: 213 | UnixTransport transport 214 | uv.uv_connect_t _req_data 215 | 216 | def __cinit__(self, loop, transport): 217 | self.request = &self._req_data 218 | self.request.data = self 219 | self.transport = transport 220 | 221 | cdef connect(self, char* addr): 222 | # uv_pipe_connect returns void 223 | uv.uv_pipe_connect(self.request, 224 | self.transport._handle, 225 | addr, 226 | __pipe_connect_callback) 227 | 228 | cdef void __pipe_connect_callback( 229 | uv.uv_connect_t* req, 230 | int status, 231 | ) noexcept with gil: 232 | cdef: 233 | _PipeConnectRequest wrapper 234 | UnixTransport transport 235 | 236 | wrapper = <_PipeConnectRequest> req.data 237 | transport = wrapper.transport 238 | 239 | if status < 0: 240 | exc = convert_error(status) 241 | else: 242 | exc = None 243 | 244 | try: 245 | transport._on_connect(exc) 246 | except BaseException as ex: 247 | wrapper.transport._fatal_error(ex, False) 248 | finally: 249 | wrapper.on_done() 250 | -------------------------------------------------------------------------------- /winloop/handles/basetransport.pyx: -------------------------------------------------------------------------------- 1 | cdef class UVBaseTransport(UVSocketHandle): 2 | 3 | def __cinit__(self): 4 | # Flow control 5 | self._high_water = FLOW_CONTROL_HIGH_WATER * 1024 6 | self._low_water = FLOW_CONTROL_HIGH_WATER // 4 7 | 8 | self._protocol = None 9 | self._protocol_connected = 0 10 | self._protocol_paused = 0 11 | self._protocol_data_received = None 12 | 13 | self._server = None 14 | self._waiter = None 15 | self._extra_info = None 16 | 17 | self._conn_lost = 0 18 | 19 | self._closing = 0 20 | 21 | cdef size_t _get_write_buffer_size(self): 22 | return 0 23 | 24 | cdef inline _schedule_call_connection_made(self): 25 | self._loop._call_soon_handle( 26 | new_MethodHandle(self._loop, 27 | "UVTransport._call_connection_made", 28 | self._call_connection_made, 29 | self.context, 30 | self)) 31 | 32 | cdef inline _schedule_call_connection_lost(self, exc): 33 | self._loop._call_soon_handle( 34 | new_MethodHandle1(self._loop, 35 | "UVTransport._call_connection_lost", 36 | self._call_connection_lost, 37 | self.context, 38 | self, exc)) 39 | 40 | cdef _fatal_error(self, exc, throw, reason=None): 41 | # Overload UVHandle._fatal_error 42 | 43 | self._force_close(exc) 44 | 45 | if not isinstance(exc, OSError): 46 | 47 | if throw or self._loop is None: 48 | raise exc 49 | 50 | msg = f'Fatal error on transport {self.__class__.__name__}' 51 | if reason is not None: 52 | msg = f'{msg} ({reason})' 53 | 54 | self._loop.call_exception_handler({ 55 | 'message': msg, 56 | 'exception': exc, 57 | 'transport': self, 58 | 'protocol': self._protocol, 59 | }) 60 | 61 | cdef inline _maybe_pause_protocol(self): 62 | cdef: 63 | size_t size = self._get_write_buffer_size() 64 | 65 | if size <= self._high_water: 66 | return 67 | 68 | if not self._protocol_paused: 69 | self._protocol_paused = 1 70 | try: 71 | # _maybe_pause_protocol() is always triggered from user-calls, 72 | # so we must copy the context to avoid entering context twice 73 | run_in_context( 74 | self.context.copy(), self._protocol.pause_writing, 75 | ) 76 | except (KeyboardInterrupt, SystemExit): 77 | raise 78 | except BaseException as exc: 79 | self._loop.call_exception_handler({ 80 | 'message': 'protocol.pause_writing() failed', 81 | 'exception': exc, 82 | 'transport': self, 83 | 'protocol': self._protocol, 84 | }) 85 | 86 | cdef inline _maybe_resume_protocol(self): 87 | cdef: 88 | size_t size = self._get_write_buffer_size() 89 | 90 | if self._protocol_paused and size <= self._low_water: 91 | self._protocol_paused = 0 92 | try: 93 | # We're copying the context to avoid entering context twice, 94 | # even though it's not always necessary to copy - it's easier 95 | # to copy here than passing down a copied context. 96 | run_in_context( 97 | self.context.copy(), self._protocol.resume_writing, 98 | ) 99 | except (KeyboardInterrupt, SystemExit): 100 | raise 101 | except BaseException as exc: 102 | self._loop.call_exception_handler({ 103 | 'message': 'protocol.resume_writing() failed', 104 | 'exception': exc, 105 | 'transport': self, 106 | 'protocol': self._protocol, 107 | }) 108 | 109 | cdef _wakeup_waiter(self): 110 | if self._waiter is not None: 111 | if not self._waiter.cancelled(): 112 | if not self._is_alive(): 113 | self._waiter.set_exception( 114 | RuntimeError( 115 | 'closed Transport handle and unset waiter')) 116 | else: 117 | self._waiter.set_result(True) 118 | self._waiter = None 119 | 120 | cdef _call_connection_made(self): 121 | if self._protocol is None: 122 | raise RuntimeError( 123 | 'protocol is not set, cannot call connection_made()') 124 | 125 | # We use `_is_alive()` and not `_closing`, because we call 126 | # `transport._close()` in `loop.create_connection()` if an 127 | # exception happens during `await waiter`. 128 | if not self._is_alive(): 129 | # A connection waiter can be cancelled between 130 | # 'await loop.create_connection()' and 131 | # `_schedule_call_connection_made` and 132 | # the actual `_call_connection_made`. 133 | self._wakeup_waiter() 134 | return 135 | 136 | # Set _protocol_connected to 1 before calling "connection_made": 137 | # if transport is aborted or closed, "connection_lost" will 138 | # still be scheduled. 139 | self._protocol_connected = 1 140 | 141 | try: 142 | self._protocol.connection_made(self) 143 | except BaseException: 144 | self._wakeup_waiter() 145 | raise 146 | 147 | if not self._is_alive(): 148 | # This might happen when "transport.abort()" is called 149 | # from "Protocol.connection_made". 150 | self._wakeup_waiter() 151 | return 152 | 153 | self._start_reading() 154 | self._wakeup_waiter() 155 | 156 | cdef _call_connection_lost(self, exc): 157 | if self._waiter is not None: 158 | if not self._waiter.done(): 159 | self._waiter.set_exception(exc) 160 | self._waiter = None 161 | 162 | if self._closed: 163 | # The handle is closed -- likely, _call_connection_lost 164 | # was already called before. 165 | return 166 | 167 | try: 168 | if self._protocol_connected: 169 | self._protocol.connection_lost(exc) 170 | finally: 171 | self._clear_protocol() 172 | 173 | self._close() 174 | 175 | server = self._server 176 | if server is not None: 177 | (server)._detach() 178 | self._server = None 179 | 180 | cdef inline _set_server(self, Server server): 181 | self._server = server 182 | (server)._attach() 183 | 184 | cdef inline _set_waiter(self, object waiter): 185 | if waiter is not None and not isfuture(waiter): 186 | raise TypeError( 187 | f'invalid waiter object {waiter!r}, expected asyncio.Future') 188 | 189 | self._waiter = waiter 190 | 191 | cdef _set_protocol(self, object protocol): 192 | self._protocol = protocol 193 | # Store a reference to the bound method directly 194 | try: 195 | self._protocol_data_received = protocol.data_received 196 | except AttributeError: 197 | pass 198 | 199 | cdef _clear_protocol(self): 200 | self._protocol = None 201 | self._protocol_data_received = None 202 | 203 | cdef inline _init_protocol(self): 204 | self._loop._track_transport(self) 205 | if self._protocol is None: 206 | raise RuntimeError('invalid _init_protocol call') 207 | self._schedule_call_connection_made() 208 | 209 | cdef inline _add_extra_info(self, str name, object obj): 210 | if self._extra_info is None: 211 | self._extra_info = {} 212 | self._extra_info[name] = obj 213 | 214 | cdef bint _is_reading(self): 215 | raise NotImplementedError 216 | 217 | cdef _start_reading(self): 218 | raise NotImplementedError 219 | 220 | cdef _stop_reading(self): 221 | raise NotImplementedError 222 | 223 | # === Public API === 224 | 225 | property _paused: 226 | # Used by SSLProto. Might be removed in the future. 227 | def __get__(self): 228 | return bool(not self._is_reading()) 229 | 230 | def get_protocol(self): 231 | return self._protocol 232 | 233 | def set_protocol(self, protocol): 234 | self._set_protocol(protocol) 235 | if self._is_reading(): 236 | self._stop_reading() 237 | self._start_reading() 238 | 239 | def _force_close(self, exc): 240 | # Used by SSLProto. Might be removed in the future. 241 | if self._conn_lost or self._closed: 242 | return 243 | if not self._closing: 244 | self._closing = 1 245 | self._stop_reading() 246 | self._conn_lost += 1 247 | self._schedule_call_connection_lost(exc) 248 | 249 | def abort(self): 250 | self._force_close(None) 251 | 252 | def close(self): 253 | if self._closing or self._closed: 254 | return 255 | 256 | self._closing = 1 257 | self._stop_reading() 258 | 259 | if not self._get_write_buffer_size(): 260 | # The write buffer is empty 261 | self._conn_lost += 1 262 | self._schedule_call_connection_lost(None) 263 | 264 | def is_closing(self): 265 | return self._closing 266 | 267 | def get_write_buffer_size(self): 268 | return self._get_write_buffer_size() 269 | 270 | def set_write_buffer_limits(self, high=None, low=None): 271 | self._ensure_alive() 272 | 273 | self._high_water, self._low_water = add_flowcontrol_defaults( 274 | high, low, FLOW_CONTROL_HIGH_WATER) 275 | 276 | self._maybe_pause_protocol() 277 | 278 | def get_write_buffer_limits(self): 279 | return (self._low_water, self._high_water) 280 | 281 | def get_extra_info(self, name, default=None): 282 | if self._extra_info is not None and name in self._extra_info: 283 | return self._extra_info[name] 284 | if name == 'socket': 285 | return self._get_socket() 286 | if name == 'sockname': 287 | return self._get_socket().getsockname() 288 | if name == 'peername': 289 | try: 290 | return self._get_socket().getpeername() 291 | except socket_error: 292 | return default 293 | return default 294 | -------------------------------------------------------------------------------- /tests/test_pipes.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | import os 4 | import socket 5 | import sys 6 | import unittest 7 | 8 | from winloop import _testbase as tb 9 | 10 | # All tests are copied from asyncio (mostly as-is) 11 | 12 | 13 | class MyReadPipeProto(asyncio.Protocol): 14 | done = None 15 | 16 | def __init__(self, loop=None): 17 | self.state = ["INITIAL"] 18 | self.nbytes = 0 19 | self.transport = None 20 | if loop is not None: 21 | self.done = asyncio.Future(loop=loop) 22 | 23 | def connection_made(self, transport): 24 | self.transport = transport 25 | assert self.state == ["INITIAL"], self.state 26 | self.state.append("CONNECTED") 27 | 28 | def data_received(self, data): 29 | assert self.state == ["INITIAL", "CONNECTED"], self.state 30 | self.nbytes += len(data) 31 | 32 | def eof_received(self): 33 | assert self.state == ["INITIAL", "CONNECTED"], self.state 34 | self.state.append("EOF") 35 | 36 | def connection_lost(self, exc): 37 | if "EOF" not in self.state: 38 | self.state.append("EOF") # It is okay if EOF is missed. 39 | assert self.state == ["INITIAL", "CONNECTED", "EOF"], self.state 40 | self.state.append("CLOSED") 41 | if self.done: 42 | self.done.set_result(None) 43 | 44 | 45 | class MyWritePipeProto(asyncio.BaseProtocol): 46 | done = None 47 | paused = False 48 | 49 | def __init__(self, loop=None): 50 | self.state = "INITIAL" 51 | self.transport = None 52 | if loop is not None: 53 | self.done = asyncio.Future(loop=loop) 54 | 55 | def connection_made(self, transport): 56 | self.transport = transport 57 | assert self.state == "INITIAL", self.state 58 | self.state = "CONNECTED" 59 | 60 | def connection_lost(self, exc): 61 | assert self.state == "CONNECTED", self.state 62 | self.state = "CLOSED" 63 | if self.done: 64 | self.done.set_result(None) 65 | 66 | def pause_writing(self): 67 | self.paused = True 68 | 69 | def resume_writing(self): 70 | self.paused = False 71 | 72 | 73 | # Winloop comment: on Windows the asyncio event loop does not support pipes. 74 | # For instance, running in ..\Lib\test\test_asyncio the unit test 75 | # test_events.ProactorEventLoopTests.test_unclosed_pipe_transport 76 | # gives: ... skipped "Don't support pipes for Windows" 77 | # See also: https://github.com/python/cpython/issues/71019 from that: 78 | # "On Windows, sockets and named pipes are supported, on Linux fifo, sockets, 79 | # pipes and character devices are supported, no idea about macOS." 80 | class _BasePipeTest: 81 | def test_read_pipe(self): 82 | if sys.platform == "win32" and self.is_asyncio_loop(): 83 | raise unittest.SkipTest("do not support pipes for Windows") 84 | 85 | proto = MyReadPipeProto(loop=self.loop) 86 | 87 | rpipe, wpipe = os.pipe() 88 | pipeobj = io.open(rpipe, "rb", 1024) 89 | 90 | async def connect(): 91 | t, p = await self.loop.connect_read_pipe(lambda: proto, pipeobj) 92 | self.assertIs(p, proto) 93 | self.assertIs(t, proto.transport) 94 | self.assertEqual(["INITIAL", "CONNECTED"], proto.state) 95 | self.assertEqual(0, proto.nbytes) 96 | 97 | self.loop.run_until_complete(connect()) 98 | 99 | os.write(wpipe, b"1") 100 | tb.run_until(self.loop, lambda: proto.nbytes >= 1) 101 | self.assertEqual(1, proto.nbytes) 102 | 103 | os.write(wpipe, b"2345") 104 | tb.run_until(self.loop, lambda: proto.nbytes >= 5) 105 | self.assertEqual(["INITIAL", "CONNECTED"], proto.state) 106 | self.assertEqual(5, proto.nbytes) 107 | 108 | os.close(wpipe) 109 | self.loop.run_until_complete(proto.done) 110 | self.assertEqual(["INITIAL", "CONNECTED", "EOF", "CLOSED"], proto.state) 111 | # extra info is available 112 | self.assertIsNotNone(proto.transport.get_extra_info("pipe")) 113 | 114 | @unittest.skipIf(sys.platform == "win32", "no os.openpty on Windows") 115 | def test_read_pty_output(self): 116 | proto = MyReadPipeProto(loop=self.loop) 117 | 118 | master, slave = os.openpty() 119 | master_read_obj = io.open(master, "rb", 0) 120 | 121 | async def connect(): 122 | t, p = await self.loop.connect_read_pipe(lambda: proto, master_read_obj) 123 | self.assertIs(p, proto) 124 | self.assertIs(t, proto.transport) 125 | self.assertEqual(["INITIAL", "CONNECTED"], proto.state) 126 | self.assertEqual(0, proto.nbytes) 127 | 128 | self.loop.run_until_complete(connect()) 129 | 130 | os.write(slave, b"1") 131 | tb.run_until(self.loop, lambda: proto.nbytes) 132 | self.assertEqual(1, proto.nbytes) 133 | 134 | os.write(slave, b"2345") 135 | tb.run_until(self.loop, lambda: proto.nbytes >= 5) 136 | self.assertEqual(["INITIAL", "CONNECTED"], proto.state) 137 | self.assertEqual(5, proto.nbytes) 138 | 139 | # On Linux, transport raises EIO when slave is closed -- 140 | # ignore it. 141 | self.loop.set_exception_handler(lambda loop, ctx: None) 142 | os.close(slave) 143 | proto.transport.close() 144 | self.loop.run_until_complete(proto.done) 145 | 146 | self.assertEqual(["INITIAL", "CONNECTED", "EOF", "CLOSED"], proto.state) 147 | # extra info is available 148 | self.assertIsNotNone(proto.transport.get_extra_info("pipe")) 149 | 150 | def test_write_pipe(self): 151 | if sys.platform == "win32" and self.is_asyncio_loop(): 152 | raise unittest.SkipTest("do not support pipes for Windows") 153 | 154 | if sys.platform == "win32" and sys.version_info[:3] < (3, 12, 0): 155 | raise unittest.SkipTest("no os.set_blocking() on Windows") 156 | 157 | rpipe, wpipe = os.pipe() 158 | os.set_blocking(rpipe, False) 159 | pipeobj = io.open(wpipe, "wb", 1024) 160 | 161 | proto = MyWritePipeProto(loop=self.loop) 162 | connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) 163 | transport, p = self.loop.run_until_complete(connect) 164 | self.assertIs(p, proto) 165 | self.assertIs(transport, proto.transport) 166 | self.assertEqual("CONNECTED", proto.state) 167 | 168 | transport.write(b"1") 169 | 170 | data = bytearray() 171 | 172 | def reader(data): 173 | try: 174 | chunk = os.read(rpipe, 1024) 175 | except BlockingIOError: 176 | return len(data) 177 | data += chunk 178 | return len(data) 179 | 180 | tb.run_until(self.loop, lambda: reader(data) >= 1) 181 | self.assertEqual(b"1", data) 182 | 183 | transport.write(b"2345") 184 | tb.run_until(self.loop, lambda: reader(data) >= 5) 185 | self.assertEqual(b"12345", data) 186 | self.assertEqual("CONNECTED", proto.state) 187 | 188 | os.close(rpipe) 189 | 190 | # extra info is available 191 | self.assertIsNotNone(proto.transport.get_extra_info("pipe")) 192 | 193 | # close connection 194 | proto.transport.close() 195 | self.loop.run_until_complete(proto.done) 196 | self.assertEqual("CLOSED", proto.state) 197 | 198 | @unittest.skipIf(sys.platform == "win32", "no Unix sockets on Windows") 199 | def test_write_pipe_disconnect_on_close(self): 200 | rsock, wsock = socket.socketpair() 201 | rsock.setblocking(False) 202 | 203 | pipeobj = io.open(wsock.detach(), "wb", 1024) 204 | 205 | proto = MyWritePipeProto(loop=self.loop) 206 | connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) 207 | transport, p = self.loop.run_until_complete(connect) 208 | self.assertIs(p, proto) 209 | self.assertIs(transport, proto.transport) 210 | self.assertEqual("CONNECTED", proto.state) 211 | 212 | transport.write(b"1") 213 | data = self.loop.run_until_complete(self.loop.sock_recv(rsock, 1024)) 214 | self.assertEqual(b"1", data) 215 | 216 | rsock.close() 217 | 218 | self.loop.run_until_complete(proto.done) 219 | self.assertEqual("CLOSED", proto.state) 220 | 221 | @unittest.skipIf(sys.platform == "win32", "no os.openpty on Windows") 222 | def test_write_pty(self): 223 | master, slave = os.openpty() 224 | os.set_blocking(master, False) 225 | 226 | slave_write_obj = io.open(slave, "wb", 0) 227 | 228 | proto = MyWritePipeProto(loop=self.loop) 229 | connect = self.loop.connect_write_pipe(lambda: proto, slave_write_obj) 230 | transport, p = self.loop.run_until_complete(connect) 231 | self.assertIs(p, proto) 232 | self.assertIs(transport, proto.transport) 233 | self.assertEqual("CONNECTED", proto.state) 234 | 235 | transport.write(b"1") 236 | 237 | data = bytearray() 238 | 239 | def reader(data): 240 | try: 241 | chunk = os.read(master, 1024) 242 | except BlockingIOError: 243 | return len(data) 244 | data += chunk 245 | return len(data) 246 | 247 | tb.run_until(self.loop, lambda: reader(data) >= 1, timeout=10) 248 | self.assertEqual(b"1", data) 249 | 250 | transport.write(b"2345") 251 | tb.run_until(self.loop, lambda: reader(data) >= 5, timeout=10) 252 | self.assertEqual(b"12345", data) 253 | self.assertEqual("CONNECTED", proto.state) 254 | 255 | os.close(master) 256 | 257 | # extra info is available 258 | self.assertIsNotNone(proto.transport.get_extra_info("pipe")) 259 | 260 | # close connection 261 | proto.transport.close() 262 | self.loop.run_until_complete(proto.done) 263 | self.assertEqual("CLOSED", proto.state) 264 | 265 | def test_write_buffer_full(self): 266 | if sys.platform == "win32" and self.is_asyncio_loop(): 267 | raise unittest.SkipTest("do not support pipes for Windows") 268 | 269 | rpipe, wpipe = os.pipe() 270 | pipeobj = io.open(wpipe, "wb", 1024) 271 | 272 | proto = MyWritePipeProto(loop=self.loop) 273 | connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) 274 | transport, p = self.loop.run_until_complete(connect) 275 | self.assertIs(p, proto) 276 | self.assertIs(transport, proto.transport) 277 | self.assertEqual("CONNECTED", proto.state) 278 | 279 | for i in range(32): 280 | transport.write(b"x" * 32768) 281 | if proto.paused: 282 | transport.write(b"x" * 32768) 283 | break 284 | else: 285 | self.fail("Didn't reach a full buffer") 286 | 287 | os.close(rpipe) 288 | self.loop.run_until_complete(asyncio.wait_for(proto.done, 1)) 289 | self.assertEqual("CLOSED", proto.state) 290 | 291 | 292 | class Test_UV_Pipes(_BasePipeTest, tb.UVTestCase): 293 | pass 294 | 295 | 296 | class Test_AIO_Pipes(_BasePipeTest, tb.AIOTestCase): 297 | pass 298 | --------------------------------------------------------------------------------