1
0
Fork 0
mirror of https://gitee.com/fantix/kloop.git synced 2024-05-23 18:58:39 +00:00

Compare commits

...

32 commits
v0.0.1 ... main

Author SHA1 Message Date
Fantix King f055323af3
Update gitignore 2022-07-02 18:11:03 -04:00
Fantix King bfc3fb451a
Unify debug setting 2022-07-02 18:08:25 -04:00
Fantix King 479d8be584
Add debug script and certs 2022-07-02 18:08:07 -04:00
Fantix King d659f7d533
Fix metadata to point to Gitee 2022-07-02 16:10:08 -04:00
Fantix King 652efd90a4
Fix README about TLS 1.3 RX offloading 2022-07-02 16:08:49 -04:00
Fantix King 3c2d02a3f9
Fix TLS 1.3 control messages after handshake 2022-07-02 15:49:43 -04:00
Fantix King 761f741d5d
TLS: fix buffer usage and reset message
Also improved debug
2022-07-02 14:37:19 -04:00
Fantix King 74f0062154
Barely working TLS client!
Refs #I5ANZH
2022-06-26 11:48:49 -04:00
Fantix King a1094281ec
Fix a param set-zero bug when io_uring_enter returns EINVAL
Also we don't need to set `to_submit` when SQPOLL is set
2022-06-26 11:48:49 -04:00
Fantix King 081a2877b8
Fix setup.py build and clean 2022-06-26 11:48:48 -04:00
Fantix King 665647013b
Initially added TLSTransport
Refs #I5ANZH
2022-06-26 11:48:48 -04:00
Fantix King 47636fbf04
Extract TCP connect
Fixes #I5ANZE, but not in nogil for simplicity for now
2022-06-26 11:48:48 -04:00
Fantix King db84313627
Fix logo position on mobile display 2022-06-26 11:48:48 -04:00
Fantix King 08d3d8ff9a
Add GitHub Action to build 2022-06-26 11:48:48 -04:00
Fantix King a8f639fdce
Add logo and badge 2022-06-26 11:48:46 -04:00
Fantix King 19803cd298
Rename kloop.ktls to kloop.tls 2022-06-26 11:48:35 -04:00
Fantix King 9bdf0d1834
Update URL in the license text 2022-06-26 11:48:34 -04:00
Fantix King a935db4858
Clean up Rust project 2022-06-26 11:48:34 -04:00
Fantix King eca322da58
Add dev steps in README
* Also fixed issues in setup.cfg
* Dropped unnecessary deps in Cargo.toml
2022-06-26 11:48:34 -04:00
Fantix King 6ca4161a46
Correct links and images in README 2022-06-26 11:48:32 -04:00
Fantix King 85799a1624
GitHub doesn't support multilingual 2022-06-26 11:48:07 -04:00
Fantix King 9cf03082c8
Fix README about modprobe tls 2022-06-26 11:48:07 -04:00
Fantix King 91691e5996
Implement UDP recv, basic DNS working! 2022-06-26 11:48:07 -04:00
Fantix King f3c43bf145
Use atomic API 2022-06-26 11:48:07 -04:00
Fantix King dd4a38a610
WIP resolver 2022-06-26 11:48:06 -04:00
Fantix King 3b4b6c5c67
Update README and bump to 0.2 2022-06-26 11:48:03 -04:00
Fantix King 64b105657a
Socket connect PoC 2022-04-25 08:02:28 -04:00
Fantix King bb7c378c95
New implementation: nogil for most of the stuff 2022-04-23 14:42:56 -04:00
Fantix King 67c2c9aa2a
Update README 2022-04-19 18:50:09 -04:00
Fantix King 9fc44a51c8
Almost working PoC 2022-04-19 18:32:40 -04:00
Fantix King 97f02d40bc
PoC uring and ktls 2022-03-20 19:07:05 -04:00
Fantix King 04b59cc52e
Update README and add Makefile 2022-03-19 17:41:48 -04:00
53 changed files with 5016 additions and 34 deletions

32
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: 构建
on: [push]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: 获取源代码
uses: actions/checkout@v3
- name: 安装 Python
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: 安装依赖关系
run: |
python -m pip install --upgrade pip
python -m pip install build
- name: 构建软件包
run: python -m build
- name: 上传源码包
uses: actions/upload-artifact@v3
with:
name: kLoop 源码包
path: dist/*.tar.gz
- name: 上传 wheel 安装包
uses: actions/upload-artifact@v3
with:
name: kLoop wheel 安装包
path: dist/*.whl

3
.gitignore vendored
View file

@ -6,4 +6,5 @@
/tests/**/__pycache__/
/src/**/*.c
/src/**/*.so
/src/kloop.egg-info/
/src/kLoop.egg-info/
/resolver/target/

View file

@ -1,5 +1,5 @@
版权所有 (c) 2022 王川 http://fantix.pro
Copyright (c) 2022 Fantix King http://fantix.pro
版权所有 (c) 2022 王川 https://fantix.pro
Copyright (c) 2022 Fantix King https://fantix.pro
木兰宽松许可证

View file

@ -1,4 +1,9 @@
recursive-include src *.pyx *.pxd
include README*.md
include Makefile
recursive-include docs *.png *.md
recursive-include src *.pyx *.pxd *.h
include resolver/Cargo.toml
include resolver/Cargo.lock
recursive-include resolver *.rs
graft tests
global-exclude *.py[cod] *.c

23
Makefile Normal file
View file

@ -0,0 +1,23 @@
.PHONY: dist dev build clean
.DEFAULT_GOAL := dev
# Make distribution tarballs
dist:
pip install -U build
python -m build
# Incrementally build for development
dev:
KLOOP_DEBUG=1 python setup.py develop
# Always build for development
build:
KLOOP_FORCE=1 KLOOP_DEBUG=1 python setup.py develop
# Clean up everything including the Rust build cache
clean:
python setup.py clean --all

View file

@ -1,5 +1,73 @@
# kLoop
# <img src="docs/kloop@2x.png" height="96px" align="left"> kLoop - *asyncio on Linux kernel*
kLoop is an implementation of the Python asyncio event loop written in Cython,
using io_uring and kTLS features of the Linux kernel, open-sourced and released
under the MulanPSL - 2.0 license.
[![中文](https://img.shields.io/badge/Zh-中文-informational?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAFKADAAQAAAABAAAAEAAAAABHXVY9AAABc0lEQVQ4EaWSOy8EURTHd+wDEY94JVtgg9BI1B6dQqHiE1CrRasT30DpC2hVQimiFkJWVsSj2U1EsmQH4/ff3CO3WDuDk/zmf8/jnntm7qRSMRZFUQ4WYSimNFmaRlsgq8F83K6WuALyva4mixbc+kfJcGqa7CqU4AjaocNpG5oHsx7qB3EqQRC8K4g/gazAMbFTBdbgL1Zh0w2EbnMVHdMrd4LZNotZmIZJKMAemC2z0MS6oDlYhzOQ6c3yGR5Fec4OGPvEHCmn3np+kfyT51+QH8afcbFLTfjgFVS9tZrpwC4v1k9M39w3NTQrBxSM4127SAmNoBt0Ma3QyHRwGUIYdQUh0+c0wZsLPKKH8AwvoHgNlmABZLtwBdqnP0DD9IEG2If6N0oz5SbYSfW4PYhvgNmUxU1JZGEEAsUyjPmB7lhBA1Xe7NMWpuzXa39fnC7lN1b/mZttSNLQv9XXZs2US9LwzjU5R+/d+n/CBx9I2uELeXrRajeDqHwAAAAASUVORK5CYII=)](README.zh.md)
[![build](https://img.shields.io/github/workflow/status/fantix/kloop/构建?label=build&logo=github)](https://github.com/fantix/kloop/actions/workflows/build.yml)
[![downloads](https://img.shields.io/pypi/dm/kloop?logo=pypi&logoColor=white)](https://pypi.python.org/pypi/gino)
[![code quality](https://img.shields.io/codacy/grade/f2e97d6eb2554e87b3cd15aae8f6b1e0?logo=codacy)](https://app.codacy.com/gh/fantix/kloop/dashboard)
[![license](https://img.shields.io/badge/license-MulanPSL--2.0-success?logo=opensourceinitiative&logoColor=white)](http://license.coscl.org.cn/MulanPSL2/)
kLoop is an implementation of the Python
[asyncio](https://docs.python.org/3/library/asyncio.html) event loop written
in [Cython](https://cython.org/), using
[io_uring](https://unixism.net/loti/what_is_io_uring.html) and
[kTLS](https://www.kernel.org/doc/html/latest/networking/tls-offload.html)
features of the Linux kernel, therefore called k(ernel)Loop.
kLoop is open-sourced and released under the
[MulanPSL - 2.0 license](http://license.coscl.org.cn/MulanPSL2).
**⚠WARNING: THIS PROJECT IS IN PROOF-OF-CONCEPT STAGE!⚠️**
## Features
* **Minimum syscalls** - all I/O calls are done in the kernel thanks to
io_uring, and the only remaining syscall to `io_uring_enter()` is also
optimized to be only called when necessary using the `SQPOLL` feature. That
means most of the overhead of syscalls is gone;
* **No GIL in the main-loop** - the hot-path is written in Cython without GIL,
that means it's compiled into pure C code without Python structures, saving
some memory and execution time. On the other hand, GIL is only taken before
Python callbacks, so it's slightly more friendly to multithreading, which is
still not recommended though.
* **TLS offloading** - all symmetric-key encryption and decryption work is
offloaded to the NIC if supported, or to the thread pool of io_uring
otherwise. This allows us to free up CPU for more I/O, or leverage all the
CPU cores even if the application is running single-threaded.
* **Asynchronous DNS resolving** - we blended in the Rust
[trust-dns](https://github.com/bluejekyll/trust-dns/) library with a custom
I/O runtime bridging to io_uring in C (including reading the
`/etc/resolv.conf` and `/etc/hosts` files), providing flexible APIs to manage
the concurrency, cache and configuration in Python.
## Requirements
* Python >= 3.10
* Linux >= 5.11 (enable kTLS with `modprobe tls`)
* OpenSSL >= 3.0 (kTLS receive offloading on TLS 1.3 requires the latest
development version)
Development and testing is done on Ubuntu 22.04.
## Architecture Diagram
<img src="docs/architecture.en.png" width="620px" alt="architecture.png">
Looks like the Lucky Charms factory, says @aaronbrighton ...
## Development
### Ubuntu 22.04
```bash
sudo apt update
sudo apt install gcc libssl-dev python3-dev python3.10-venv
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
python3 -m venv path/to/env
source path/to/env/bin/activate
pip install cython
KLOOP_DEBUG=1 python setup.py develop # or just run `make`
```

67
README.zh.md Normal file
View file

@ -0,0 +1,67 @@
# <img src="docs/kloop@2x.png" height="80px" align="left"> kLoop*Linux 内核上的 asyncio*
[![English](https://img.shields.io/badge/英文-English-informational?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAQCAYAAADnEwSWAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAG6ADAAQAAAABAAAAEAAAAACiF0fSAAABJUlEQVQ4EWP8//8/MwMDAysQEw0YGRl/EK0YWSHQsgogJgX8RNZPCpuJFMWUqqWrZSw4XBsCFL+FQ+4/DnEUYWC8gNKCEDB+X8MlgIKVWCJMD64ADwOorwmIHyNhdyDbFYgPAfFXIAaBV0CcCTIG5DNsLo0GKnDEYc8+oGsvQ+UEgLQMkrpYIDsSiJGjRxTInwY07wmuYCxDMgCdmQUUgFmGLheNLoDEzwG5gBFJgFQmNr3ZQEPsgfgImmEquHy2BajwHZpiGPcmjAGk0aPgCDCIp4HkgcHWA6RsQGwoEMEVZ9VATZdgqkig7yKp/YjEBjORIxJdjhw+3tIFVzCGAoPBEo9ta4E+f4NHHqsULstqsKpGCJ4BMkm2jNrBiHAOFhZdLQMA8pKhkQYZiokAAAAASUVORK5CYII=)](README.md)
[![构建](https://img.shields.io/github/workflow/status/fantix/kloop/构建?label=构建&logo=github)](https://github.com/fantix/kloop/actions/workflows/build.yml)
[![下载](https://img.shields.io/pypi/dm/kloop?logo=pypi&logoColor=white&label=下载)](https://pypi.python.org/pypi/gino)
[![质量](https://img.shields.io/codacy/grade/f2e97d6eb2554e87b3cd15aae8f6b1e0?logo=codacy&label=质量)](https://app.codacy.com/gh/fantix/kloop/dashboard)
[![许可](https://img.shields.io/badge/许可-木兰PSLv2-success?logo=opensourceinitiative&logoColor=white)](http://license.coscl.org.cn/MulanPSL2/)
kLoop 是一个 Python
[asyncio](https://docs.python.org/3/library/asyncio.html)
事件循环的实现,主要用 [Cython](https://cython.org/) 编写,重点使用了 Linux 内核的
[io_uring](https://unixism.net/loti/what_is_io_uring.html) 和
[kTLS](https://www.kernel.org/doc/html/latest/networking/tls-offload.html)
功能,故称作 k(ernel)Loop。
您可在[木兰宽松许可证, 第2版](http://license.coscl.org.cn/MulanPSL2)允许的范围内使用
kLoop 的源代码或发行版。
**⚠️警告:项目仍在概念验证当中,满地都是坑!⚠️**
## 主要特性
* **系统调用次数非常少**:得益于 io_uring所有 I/O 调用都由内核完成;因为启用了
`SQPOLL`,唯一的系统调用 `io_uring_enter()`
也仅在必要时才会用到,因此节省下了几乎所有系统调用的额外开销;
* **主循环主体不占有 GIL**:虽然使用了 Cython 编写,但大部分最核心的代码都被编译为不需要
Python 结构的普通 C 代码,一方面节省内存开销和处理时间,另一方面不持有 GIL仅在有
Python 回调时才上 GIL 锁,对多线程稍微友好一点,但还是推荐单线程跑;
* **内核网卡代工 TLS**如果硬件支持TLS 通讯的全部对称加密解密皆由网卡完成,空出 CPU
来做更多的 I/O即使网卡不支持io_uring
也可以在内核中开多线程来执行对称加解密,不受应用端单线程的限制,充分利用多核 CPU
* **纯异步 DNS 解析**:混编进了 Rust 写的
[trust-dns](https://github.com/bluejekyll/trust-dns/)I/O 层直接走
io_uring包括加载 `/etc/resolv.conf``/etc/hosts` 文件),在 C 和 Rust
之间互相调用完成 DNS 解析,并且提供了更加灵活的 Python 接口来控制并发、缓存和配置文件。
## 环境需求
* Python >= 3.10
* Linux >= 5.11 (用 `modprobe tls` 命令来启用 kTLS 模块)
* OpenSSL >= 3.0TLS 1.3 支持 kTLS 收包代工需要最新的开发版本)
目前主要是在 Ubuntu 22.04 上开发测试的。
## 架构图
<img src="docs/architecture.zh.png" width="620px" alt="架构图.png">
@aaronbrighton 说像 Lucky Charms 卡通麦片工厂……
## 开发
### Ubuntu 22.04
```bash
sudo apt update
sudo apt install gcc libssl-dev python3-dev python3.10-venv
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
python3 -m venv path/to/env
source path/to/env/bin/activate
pip install cython
KLOOP_DEBUG=1 python setup.py develop # 或者直接执行 `make`
```

BIN
docs/architecture.en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

BIN
docs/architecture.zh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
docs/kloop@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -1,3 +1,7 @@
[tool.black]
line-length = 79
target-version = ["py310"]
[build-system]
requires = ["setuptools>=42", "Cython>=0.29"]
build-backend = "setuptools.build_meta"

645
resolver/Cargo.lock generated Normal file
View file

@ -0,0 +1,645 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "async-trait"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "enum-as-inner"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "432f044e9077ad6666f3446df0489a71ac3ed0336ea54edf6b85e00cd7562283"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "ipconfig"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98"
dependencies = [
"socket2",
"widestring",
"winapi",
"winreg",
]
[[package]]
name = "ipnet"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
dependencies = [
"cfg-if",
]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "once_cell"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "parking_lot"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro2"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
dependencies = [
"bitflags",
]
[[package]]
name = "resolv-conf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
"quick-error",
]
[[package]]
name = "resolver"
version = "0.1.0"
dependencies = [
"async-trait",
"futures-executor",
"futures-io",
"futures-util",
"libc",
"log",
"resolv-conf",
"trust-dns-proto",
"trust-dns-resolver",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "slab"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "smallvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "socket2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tracing"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921"
dependencies = [
"once_cell",
]
[[package]]
name = "trust-dns-proto"
version = "0.21.2"
source = "git+https://github.com/bluejekyll/trust-dns#170e3f7d7dfbc2b6c48318cb5c996a8cc6db77a1"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"lazy_static",
"rand",
"smallvec",
"thiserror",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.21.2"
source = "git+https://github.com/bluejekyll/trust-dns#170e3f7d7dfbc2b6c48318cb5c996a8cc6db77a1"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"lru-cache",
"parking_lot",
"resolv-conf",
"smallvec",
"thiserror",
"tracing",
"trust-dns-proto",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "widestring"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
[[package]]
name = "windows_i686_gnu"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
[[package]]
name = "windows_i686_msvc"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
[[package]]
name = "windows_x86_64_gnu"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
[[package]]
name = "windows_x86_64_msvc"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
[[package]]
name = "winreg"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
dependencies = [
"winapi",
]

19
resolver/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "resolver"
version = "0.1.0"
edition = "2021"
[lib]
name = "kloop_resolver"
crate-type = ["staticlib"]
[dependencies]
libc = "0.2.124"
log = "0.4.16"
resolv-conf = { version = "0.7.0", features = ["system"] }
trust-dns-proto = { git = "https://github.com/bluejekyll/trust-dns", default-features = false }
trust-dns-resolver = { git = "https://github.com/bluejekyll/trust-dns", default-features = false, features = ["system-config"]}
futures-util = "0.3.21"
futures-io = "0.3.5"
futures-executor = "0.3.5"
async-trait = "0.1.43"

61
resolver/src/kloop.rs Normal file
View file

@ -0,0 +1,61 @@
/*
Copyright (c) 2022 Fantix King https://fantix.pro
kLoop is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
use core::marker;
use std::task::Waker;
use libc;
use crate::resolve::KLoopResolver;
#[repr(C)]
pub struct CResolver {
_data: [u8; 0],
_marker: marker::PhantomData<(*mut u8, marker::PhantomPinned)>,
}
#[repr(C)]
pub struct CResolve {
_data: [u8; 0],
_marker: marker::PhantomData<(*mut u8, marker::PhantomPinned)>,
}
#[repr(C)]
pub struct UDPAction {
_data: [u8; 0],
_marker: marker::PhantomData<(*mut u8, marker::PhantomPinned)>,
}
extern "C" {
pub fn resolver_set(c_resolver: *const CResolver, resolver: *mut KLoopResolver);
// pub fn resolve_set_poller(resolve: *const CResolve, poller: *mut Poller);
pub fn resolve_prep_addr(resolve: *const CResolve) -> *mut libc::sockaddr;
pub fn resolve_done_cb(resolve: *const CResolve) -> libc::c_int;
pub fn udp_bind(addr: *const libc::sockaddr, addrlen: libc::socklen_t) -> libc::c_int;
pub fn udp_action_init(fd: libc::c_int, resolver: *const CResolver) -> libc::c_ulong;
pub fn udp_send_poll(
send: libc::c_ulong,
data: *const u8,
datalen: libc::size_t,
addr: *const libc::sockaddr,
addrlen: libc::socklen_t,
waker: *mut Waker,
) -> libc::c_int;
pub fn udp_recv_poll(
recv: libc::c_ulong,
data: *mut u8,
datalen: libc::size_t,
waker: *mut Waker,
) -> libc::c_int;
pub fn udp_get_addr(action: libc::c_ulong) -> *const libc::sockaddr;
pub fn udp_action_free(action: libc::c_ulong);
}

18
resolver/src/lib.rs Normal file
View file

@ -0,0 +1,18 @@
/*
Copyright (c) 2022 Fantix King https://fantix.pro
kLoop is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
mod kloop;
mod resolve;
mod runtime;
pub use resolve::{resolver_init, resolver_lookup_ip};

183
resolver/src/resolve.rs Normal file
View file

@ -0,0 +1,183 @@
/*
Copyright (c) 2022 Fantix King https://fantix.pro
kLoop is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
use futures_executor::{LocalPool, LocalSpawner};
use futures_util::task::{LocalSpawnExt, SpawnError};
use std;
use std::cell::RefCell;
use std::future::Future;
use std::io;
use std::net::IpAddr;
use std::rc::Rc;
use libc;
use trust_dns_resolver::name_server::{GenericConnection, GenericConnectionProvider};
use trust_dns_resolver::system_conf::parse_resolv_conf;
use trust_dns_resolver::{AsyncResolver, Hosts};
use crate::kloop::{resolve_done_cb, resolve_prep_addr, resolver_set, CResolve, CResolver};
use crate::runtime::{KLoopHandle, KLoopRuntime};
type KLoopConnection = GenericConnection;
type KLoopConnectionProvider = GenericConnectionProvider<KLoopRuntime>;
thread_local! {
pub static CURRENT_RESOLVER: Rc<RefCell<Option<*mut KLoopResolver>>> = Rc::new(RefCell::new(None));
}
#[derive(Debug)]
pub struct KLoopResolver {
resolver: AsyncResolver<KLoopConnection, KLoopConnectionProvider>,
pub c_resolver: *const CResolver,
pool: LocalPool,
spawner: LocalSpawner,
}
impl KLoopResolver {
fn new(
resolv_conf: &[u8],
hosts_conf: &[u8],
c_resolver: *const CResolver,
) -> io::Result<Self> {
let (config, mut options) = parse_resolv_conf(resolv_conf)?;
options.use_hosts_file = false;
let conn_provider = GenericConnectionProvider::new(KLoopHandle);
let mut resolver = AsyncResolver::new_with_conn(config, options, conn_provider).unwrap();
resolver.set_hosts(Some(Hosts::default().read_hosts_conf(hosts_conf)?));
let pool = LocalPool::new();
let spawner = pool.spawner();
Ok(Self {
resolver,
c_resolver,
pool,
spawner,
})
}
pub fn spawn<Fut>(&self, future: Fut) -> Result<(), SpawnError>
where
Fut: Future<Output = ()> + 'static,
{
self.spawner.spawn_local(future)
}
fn run_until_stalled(&mut self) {
loop {
self.pool.run_until_stalled();
if !self.pool.try_run_one() {
break;
}
}
}
async fn lookup_ip(&self, resolve: *mut CResolve, host: &str, port: libc::in_port_t) -> () {
match self.resolver.lookup_ip(host).await {
Ok(result) => {
for ip in result.into_iter() {
match ip {
IpAddr::V4(ip) => unsafe {
let out = resolve_prep_addr(resolve) as *mut libc::sockaddr_in;
if out.is_null() {
println!("resolve_prep_addr returned NULL");
break;
}
(*out).sin_family = libc::AF_INET as libc::sa_family_t;
(*out).sin_addr = libc::in_addr {
s_addr: u32::from_ne_bytes(ip.octets()),
};
(*out).sin_port = port.to_be();
(*out).sin_zero = [0; 8];
},
IpAddr::V6(ip) => unsafe {
let out = resolve_prep_addr(resolve) as *mut libc::sockaddr_in6;
if out.is_null() {
println!("resolve_prep_addr returned NULL");
break;
}
(*out).sin6_family = libc::AF_INET6 as libc::sa_family_t;
(*out).sin6_addr = libc::in6_addr {
s6_addr: ip.octets(),
};
(*out).sin6_port = port.to_be();
},
}
}
}
Err(e) => {
println!("lookup_ip error: {:?}", e);
}
}
unsafe {
resolve_done_cb(resolve);
}
}
}
#[no_mangle]
pub extern "C" fn resolver_init(
c_resolver: *const CResolver,
resolv_conf_data: *const u8,
resolv_conf_data_size: libc::size_t,
hosts_conf_data: *const u8,
hosts_conf_data_size: libc::size_t,
) -> libc::c_int {
let resolv_conf =
unsafe { std::slice::from_raw_parts(resolv_conf_data, resolv_conf_data_size) };
let hosts_conf = unsafe { std::slice::from_raw_parts(hosts_conf_data, hosts_conf_data_size) };
let resolver = match KLoopResolver::new(resolv_conf, hosts_conf, c_resolver) {
Ok(resolver) => resolver,
Err(e) => return 0,
};
let rv = Box::into_raw(Box::new(resolver));
unsafe {
resolver_set(c_resolver, rv);
}
1
}
#[no_mangle]
pub extern "C" fn resolver_lookup_ip(
resolver: *mut KLoopResolver,
resolve: *mut CResolve,
host_raw: *const u8,
length: libc::size_t,
port: libc::in_port_t,
) -> libc::c_int {
let host = match std::str::from_utf8(unsafe { std::slice::from_raw_parts(host_raw, length) }) {
Ok(host) => host,
_ => return 0,
};
let r = || unsafe { resolver.as_mut() }.unwrap();
let fut = r().lookup_ip(resolve, host, port);
if let Err(e) = r().spawn(fut) {
println!("spawn error: {:?}", e);
return 0;
}
CURRENT_RESOLVER.with(|current| {
*current.borrow_mut() = Some(resolver);
r().run_until_stalled();
*current.borrow_mut() = None;
});
1
}
#[no_mangle]
pub extern "C" fn resolver_run_until_stalled(resolver: *mut KLoopResolver) {
CURRENT_RESOLVER.with(|current| {
*current.borrow_mut() = Some(resolver);
unsafe { resolver.as_mut() }.unwrap().run_until_stalled();
*current.borrow_mut() = None;
});
}

270
resolver/src/runtime.rs Normal file
View file

@ -0,0 +1,270 @@
/*
Copyright (c) 2022 Fantix King https://fantix.pro
kLoop is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
use crate::kloop;
use crate::resolve::CURRENT_RESOLVER;
use async_trait::async_trait;
use futures_io::{AsyncRead, AsyncWrite};
use libc::{sockaddr, socklen_t};
use std::fmt::Debug;
use std::future::Future;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use std::{io, mem};
use trust_dns_proto::error::ProtoError;
use trust_dns_proto::tcp::{Connect, DnsTcpStream};
use trust_dns_proto::udp::UdpSocket;
use trust_dns_proto::Time;
use trust_dns_resolver::name_server::{RuntimeProvider, Spawn};
#[derive(Clone, Copy, Debug)]
pub struct KLoopTimer {}
#[async_trait]
impl Time for KLoopTimer {
async fn delay_for(duration: Duration) {
println!("TODO: delay_for: {:?}", duration);
}
async fn timeout<F: 'static + Future + Send>(
duration: Duration,
future: F,
) -> io::Result<F::Output> {
println!("TODO: timeout: {:?}", duration);
Ok(future.await)
}
}
pub struct KLoopTcp {}
#[async_trait]
impl Connect for KLoopTcp {
async fn connect_with_bind(
addr: SocketAddr,
bind_addr: Option<SocketAddr>,
) -> io::Result<Self> {
println!("TODO: connect_with_bind: {:?} {:?}", addr, bind_addr);
todo!()
}
}
impl AsyncRead for KLoopTcp {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
println!("TODO: poll_read");
todo!()
}
}
impl AsyncWrite for KLoopTcp {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
println!("TODO: poll_write");
todo!()
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
println!("TODO: poll_flush");
todo!()
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
println!("TODO: poll_close");
todo!()
}
}
impl DnsTcpStream for KLoopTcp {
type Time = KLoopTimer;
}
pub struct KLoopUdp {
fd: libc::c_int,
send: libc::c_ulong,
recv: libc::c_ulong,
}
#[async_trait]
impl UdpSocket for KLoopUdp {
type Time = KLoopTimer;
async fn connect(addr: SocketAddr) -> io::Result<Self> {
println!("TODO: KLoopUdp: connect({})", addr);
todo!()
}
async fn connect_with_bind(addr: SocketAddr, bind_addr: SocketAddr) -> io::Result<Self> {
println!("TODO: KLoopUdp: connect_with_bind({}, {})", addr, bind_addr);
todo!()
}
async fn bind(addr: SocketAddr) -> io::Result<Self> {
let (addr_ptr, addr_len) = socket_addr_as_ptr(addr);
let fd = unsafe { kloop::udp_bind(addr_ptr, addr_len) };
CURRENT_RESOLVER.with(|resolver| {
let resolver = resolver.borrow().unwrap();
let resolver = unsafe { resolver.as_ref() }.unwrap();
let resolver = resolver.c_resolver;
let send = unsafe { kloop::udp_action_init(fd, resolver) };
let recv = unsafe { kloop::udp_action_init(fd, resolver) };
Ok(KLoopUdp { fd, send, recv })
})
}
fn poll_recv_from(
&self,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<(usize, SocketAddr)>> {
let waker = Box::new(cx.waker().clone());
match unsafe {
kloop::udp_recv_poll(
self.recv,
buf.as_mut_ptr(),
buf.len(),
Box::into_raw(waker),
)
} {
res if res > 0 => {
Poll::Ready(unsafe {
ptr_to_socket_addr(kloop::udp_get_addr(self.recv))
}.map(|addr| (res as usize, addr)))
}
_ => Poll::Pending,
}
}
fn poll_send_to(
&self,
cx: &mut Context<'_>,
buf: &[u8],
target: SocketAddr,
) -> Poll<io::Result<usize>> {
let waker = Box::new(cx.waker().clone());
let (addr, addrlen) = socket_addr_as_ptr(target);
match unsafe {
kloop::udp_send_poll(
self.send,
buf.as_ptr(),
buf.len(),
addr,
addrlen,
Box::into_raw(waker),
)
} {
res if res > 0 => {
Poll::Ready(Ok(res as usize))
}
res => {
Poll::Pending
}
}
}
}
impl Drop for KLoopUdp {
fn drop(&mut self) {
unsafe {
kloop::udp_action_free(self.send);
kloop::udp_action_free(self.recv);
}
}
}
fn socket_addr_as_ptr(addr: SocketAddr) -> (*const sockaddr, socklen_t) {
match addr {
SocketAddr::V4(ref a) => (
a as *const _ as *const _,
mem::size_of_val(a) as libc::socklen_t,
),
SocketAddr::V6(ref a) => (
a as *const _ as *const _,
mem::size_of_val(a) as libc::socklen_t,
),
}
}
unsafe fn ptr_to_socket_addr(
addr: *const libc::sockaddr,
) -> io::Result<SocketAddr> {
match (*addr).sa_family as libc::c_int {
libc::AF_INET => {
let addr: &libc::sockaddr_in = &*(addr as *const libc::sockaddr_in);
let ip = Ipv4Addr::from(addr.sin_addr.s_addr.to_ne_bytes());
let port = u16::from_be(addr.sin_port);
Ok(SocketAddr::V4(SocketAddrV4::new(ip, port)))
}
libc::AF_INET6 => {
let addr: &libc::sockaddr_in6 = &*(addr as *const libc::sockaddr_in6);
let ip = Ipv6Addr::from(addr.sin6_addr.s6_addr);
let port = u16::from_be(addr.sin6_port);
Ok(SocketAddr::V6(SocketAddrV6::new(
ip,
port,
addr.sin6_flowinfo,
addr.sin6_scope_id,
)))
}
_ => Err(io::ErrorKind::InvalidInput.into()),
}
}
#[derive(Clone, Copy)]
pub struct KLoopHandle;
impl Spawn for KLoopHandle {
fn spawn_bg<F>(&mut self, future: F)
where
F: Future<Output = Result<(), ProtoError>> + Send + 'static,
{
CURRENT_RESOLVER.with(|resolver| {
let r = resolver.borrow().unwrap();
let r = unsafe { r.as_mut() }.unwrap();
r.spawn(async {
future.await.unwrap_or_else(|e| {
println!("spawn_bg error: {:?}", e);
});
});
});
}
}
#[derive(Clone, Copy)]
pub struct KLoopRuntime;
impl RuntimeProvider for KLoopRuntime {
type Handle = KLoopHandle;
type Timer = KLoopTimer;
type Udp = KLoopUdp;
type Tcp = KLoopTcp;
}
#[no_mangle]
pub extern "C" fn waker_wake(waker: *mut Waker) {
let waker = unsafe { Box::from_raw(waker) };
waker.wake();
}
#[no_mangle]
pub extern "C" fn waker_forget(waker: *mut Waker) {
unsafe { Box::from_raw(waker) };
}

View file

@ -1,14 +1,14 @@
[metadata]
name = kLoop
version = 0.0.1
version = 0.1.0
author = Fantix King
author_email = fantix.king@gmail.com
description = An asyncio event loop using Linux io_uring and kTLS.
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/fantix/kloop
url = https://gitee.com/fantix/kloop
project_urls =
Bug Tracker = https://github.com/fantix/kloop/issues
Bug Tracker = https://gitee.com/fantix/kloop/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved
@ -18,7 +18,7 @@ classifiers =
package_dir =
= src
packages = find:
python_requires = >=3.8
python_requires = >=3.10
[options.packages.find]
where = src

138
setup.py
View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -9,16 +9,134 @@
# See the Mulan PSL v2 for more details.
from Cython.Build import cythonize
from Cython.Distutils import Extension
import os
import subprocess
import sysconfig
from setuptools import setup
from Cython.Build import cythonize
from Cython.Distutils import build_ext
from Cython.Distutils import Extension
from distutils import dir_util
from distutils import log
from distutils.command.clean import clean
class build_ext_with_resolver(build_ext):
def finalize_options(self):
self.set_undefined_options(
"build",
("debug", "debug"),
("force", "force"),
)
if self.debug is None:
self.debug = os.getenv("KLOOP_DEBUG", "0") == "1"
if self.force is None:
self.force = os.getenv("KLOOP_FORCE", "0") == "1"
for ext in self.distribution.ext_modules:
if ext.cython_directives:
ext.cython_directives["language_level"] = "3"
else:
ext.cython_directives = {"language_level": "3"}
if ext.cython_compile_time_env:
ext.cython_compile_time_env["DEBUG"] = self.debug
else:
ext.cython_compile_time_env = {"DEBUG": self.debug}
if self.debug:
if "-O0" not in ext.extra_compile_args:
ext.extra_compile_args.append("-O0")
if ext.name == "kloop.loop":
resolver = (
f"resolver/target/{'debug' if self.debug else 'release'}"
f"/libkloop_resolver.a"
)
if resolver not in ext.extra_link_args:
ext.extra_link_args.append(resolver)
if resolver not in ext.depends:
ext.depends.append(resolver)
super().finalize_options()
def run(self):
if self.force:
cmd = ["cargo", "clean", "-p", "resolver"]
if not self.debug:
cmd.append("-r")
self.announce(f"Running: {cmd}", log.INFO)
subprocess.check_call(cmd, cwd="resolver")
cmd = ["cargo", "build"]
if not self.debug:
cmd.append("-r")
self.announce(f"Running: {cmd}", log.INFO)
subprocess.check_call(cmd, cwd="resolver")
super().run()
class clean_with_resolver(clean):
def run(self):
super().run()
for d in self.distribution.package_dir.values():
self._clean_dir(d)
cmd = ["cargo", "clean"]
if not self.all:
cmd.extend(["-p", "resolver"])
self.announce(f"Running: {cmd}", log.INFO)
if not self.dry_run:
subprocess.check_call(cmd, cwd="resolver")
def _clean_dir(self, path):
for f in os.listdir(path):
name, ext = os.path.splitext(f)
real_f = os.path.join(path, f)
is_dir = os.path.isdir(real_f) and not os.path.islink(real_f)
if name == "__pycache__" or ext in {".egg-info", ".so", ".c"}:
if is_dir:
dir_util.remove_tree(real_f, dry_run=self.dry_run)
else:
self.announce(f"removing {real_f!r}", log.INFO)
if not self.dry_run:
os.remove(real_f)
elif is_dir:
self._clean_dir(real_f)
setup(
ext_modules=cythonize(
[
Extension("kloop.uring", ["src/kloop/uring.pyx"]),
Extension("kloop.ktls", ["src/kloop/ktls.pyx"]),
],
language_level="3",
)
cmdclass={
"build_ext": build_ext_with_resolver,
"clean": clean_with_resolver,
},
ext_modules=[
Extension(
"kloop.loop",
["src/kloop/loop.pyx"],
),
Extension(
"kloop.tls",
["src/kloop/tls.pyx"],
libraries=[
lib.strip().removeprefix("-l")
for lib in sysconfig.get_config_var("OPENSSL_LIBS").split()
],
include_dirs=[
d.strip().removeprefix("-I")
for d in sysconfig.get_config_var("OPENSSL_INCLUDES").split()
],
library_dirs=[
d.strip().removeprefix("-L")
for d in sysconfig.get_config_var("OPENSSL_LDFLAGS").split()
if d.strip().startswith("-L")
],
extra_link_args=[
d.strip()
for d in sysconfig.get_config_var("OPENSSL_LDFLAGS").split()
if not d.strip().startswith("-L")
],
runtime_library_dirs=(lambda x: [x] if x else [])(
sysconfig.get_config_var("OPENSSL_RPATH")
),
),
],
)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -8,3 +8,5 @@
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
from .loop import KLoop, KLoopPolicy

19
src/kloop/fileio.pxd Normal file
View file

@ -0,0 +1,19 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct FileReader:
Loop* loop
const char* path
int fd, cancelled
char* data
size_t size, offset
RingCallback ring_cb
RingCallback done_cb

116
src/kloop/fileio.pyx Normal file
View file

@ -0,0 +1,116 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef size_t FILE_READ_BUF_SIZE = 4096
cdef int file_reader_openat_cb(RingCallback* cb) nogil except 0:
cdef:
int fd = cb.res
FileReader* fr = <FileReader*>cb.data
if fd < 0:
fr.done_cb.res = fd
return fr.done_cb.callback(&fr.done_cb)
if fr.cancelled:
fr.done_cb.res = -libc.ECANCELED
return fr.done_cb.callback(&fr.done_cb)
fr.fd = fd
fr.data = <char*>PyMem_RawMalloc(FILE_READ_BUF_SIZE)
if fr.data == NULL:
fr.done_cb.res = -errno.ENOMEM
return fr.done_cb.callback(&fr.done_cb)
fr.size = FILE_READ_BUF_SIZE
fr.offset = 0
fr.ring_cb.callback = file_reader_read_cb
if ring_sq_submit_read(
&fr.loop.ring.sq,
fd,
fr.data,
FILE_READ_BUF_SIZE,
0,
&fr.ring_cb,
):
return 1
else:
fr.done_cb.res = -errno.EAGAIN
return fr.done_cb.callback(&fr.done_cb)
cdef int file_reader_read_cb(RingCallback* cb) nogil except 0:
cdef:
int read = cb.res
FileReader* fr = <FileReader*>cb.data
size_t size = fr.size
size_t offset = fr.offset
if read < 0:
fr.done_cb.res = read
return fr.done_cb.callback(&fr.done_cb)
if fr.cancelled:
fr.done_cb.res = -libc.ECANCELED
return fr.done_cb.callback(&fr.done_cb)
offset += read
if read > 0 and offset == size:
size += FILE_READ_BUF_SIZE
if PyMem_RawRealloc(fr.data, size) == NULL:
fr.done_cb.res = -errno.ENOMEM
return fr.done_cb.callback(&fr.done_cb)
fr.size = size
fr.offset = offset
if ring_sq_submit_read(
&fr.loop.ring.sq,
fr.fd,
fr.data + offset,
FILE_READ_BUF_SIZE,
offset,
&fr.ring_cb,
):
return 1
else:
fr.done_cb.res = -errno.EAGAIN
return fr.done_cb.callback(&fr.done_cb)
else:
fr.done_cb.res = 1
fr.offset = offset
return fr.done_cb.callback(&fr.done_cb)
cdef int file_reader_start(
FileReader* fr, Loop* loop, const char* path
) nogil:
fr.loop = loop
fr.done_cb.res = 0
fr.fd = 0
fr.data = NULL
fr.cancelled = 0
fr.ring_cb.callback = file_reader_openat_cb
fr.ring_cb.data = <void*>fr
return ring_sq_submit_openat(
&loop.ring.sq,
0, # dfd
path, # path
0, # flags
0, # mode
&fr.ring_cb
)
cdef int file_reader_done(FileReader* fr) nogil:
cdef int fd = fr.fd
fr.done_cb.res = 0
if fr.data != NULL:
PyMem_RawFree(fr.data)
fr.data = NULL
if fd != 0:
fr.fd = 0
return ring_sq_submit_close(&fr.loop.ring.sq, fd, NULL)
return 1

37
src/kloop/handle.pxd Normal file
View file

@ -0,0 +1,37 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef unsigned char CANCELLED_MASK = 1
cdef unsigned char SCHEDULED_MASK = 1 << 1
cdef struct Callback:
unsigned char mask
PyObject* handle
long long when
cdef class Handle:
cdef:
Callback cb
object callback
object args
KLoopImpl loop
object source_traceback
object repr
object context
cdef run(self)
cdef class TimerHandle(Handle):
cdef:
bint scheduled

152
src/kloop/handle.pyx Normal file
View file

@ -0,0 +1,152 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef class Handle:
def __init__(self, callback, args, loop, context=None):
if context is None:
context = contextvars.copy_context()
self.context = context
self.loop = loop
self.callback = callback
self.args = args
self.repr = None
self.cb.handle = <PyObject*>self
# if self._loop.get_debug():
# self._source_traceback = format_helpers.extract_stack(
# sys._getframe(1))
# else:
# self._source_traceback = None
def _repr_info(self):
info = [self.__class__.__name__]
if self.cb.mask & CANCELLED_MASK:
info.append('cancelled')
if self.callback is not None:
info.append(_format_callback_source(self.callback, self.args))
# if self._source_traceback:
# frame = self._source_traceback[-1]
# info.append(f'created at {frame[0]}:{frame[1]}')
return info
def __repr__(self):
# if self._repr is not None:
# return self._repr
info = self._repr_info()
return '<{}>'.format(' '.join(info))
def cancel(self):
if self.cb.mask & CANCELLED_MASK == 0:
self.cb.mask |= CANCELLED_MASK
# if self._loop.get_debug():
# # Keep a representation in debug mode to keep callback and
# # parameters. For example, to log the warning
# # "Executing <Handle...> took 2.5 second"
# self._repr = repr(self)
self.callback = None
self.args = None
def cancelled(self):
return self.cb.mask & CANCELLED_MASK == 1
cdef run(self):
try:
self.context.run(self.callback, *self.args)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
cb = _format_callback_source(self.callback, self.args)
msg = f'Exception in callback {cb}'
context = {
'message': msg,
'exception': exc,
'handle': self,
}
if self.source_traceback:
context['source_traceback'] = self.source_traceback
self.loop.call_exception_handler(context)
self = None # Needed to break cycles when an exception occurs.
cdef class TimerHandle(Handle):
"""Object returned by timed callback registration methods."""
def __init__(self, when, callback, args, loop, context=None):
assert when is not None
super().__init__(callback, args, loop, context)
if self.source_traceback:
del self.source_traceback[-1]
self.cb.when = when
# def _repr_info(self):
# info = super()._repr_info()
# pos = 2 if self._cancelled else 1
# info.insert(pos, f'when={self._when}')
# return info
def cancel(self):
if self.cb.mask & (CANCELLED_MASK | SCHEDULED_MASK) == SCHEDULED_MASK:
self.loop.loop.timer_cancelled_count += 1
super().cancel()
def when(self):
return self.cb.when
def _get_function_source(func):
func = inspect.unwrap(func)
if inspect.isfunction(func):
code = func.__code__
return (code.co_filename, code.co_firstlineno)
if isinstance(func, functools.partial):
return _get_function_source(func.func)
if isinstance(func, functools.partialmethod):
return _get_function_source(func.func)
return None
def _format_callback_source(func, args):
func_repr = _format_callback(func, args, None)
source = _get_function_source(func)
if source:
func_repr += f' at {source[0]}:{source[1]}'
return func_repr
def _format_args_and_kwargs(args, kwargs):
"""Format function arguments and keyword arguments.
Special case for a single parameter: ('hello',) is formatted as ('hello').
"""
# use reprlib to limit the length of the output
items = []
if args:
items.extend(reprlib.repr(arg) for arg in args)
if kwargs:
items.extend(f'{k}={reprlib.repr(v)}' for k, v in kwargs.items())
return '({})'.format(', '.join(items))
def _format_callback(func, args, kwargs, suffix=''):
if isinstance(func, functools.partial):
suffix = _format_args_and_kwargs(args, kwargs) + suffix
return _format_callback(func.func, func.args, func.keywords, suffix)
if hasattr(func, '__qualname__') and func.__qualname__:
func_repr = func.__qualname__
elif hasattr(func, '__name__') and func.__name__:
func_repr = func.__name__
else:
func_repr = repr(func)
func_repr += _format_args_and_kwargs(args, kwargs)
if suffix:
func_repr += suffix
return func_repr

15
src/kloop/heapq.pxd Normal file
View file

@ -0,0 +1,15 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct HeapQueue:
Callback** array
int size
int tail

184
src/kloop/heapq.pyx Normal file
View file

@ -0,0 +1,184 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef int HEAP_BLOCK_SIZE = 1024
cdef void siftup(HeapQueue* heap, int pos, int endpos) nogil:
cdef:
int childpos, limit = endpos >> 1
Callback** array = heap.array
while pos < limit:
childpos = 2 * pos + 1
if childpos + 1 < endpos:
if array[childpos].when >= array[childpos + 1].when:
childpos += 1
array[childpos], array[pos] = array[pos], array[childpos]
pos = childpos
siftdown(heap, pos, endpos)
cdef void siftdown(HeapQueue* heap, int pos, int size) nogil:
cdef:
int parentpos
Callback** array = heap.array
long long new_when = array[pos].when
while pos > 0:
parentpos = (pos - 1) >> 1
if new_when >= array[parentpos].when:
break
new_when = array[pos].when
array[pos], array[parentpos] = array[parentpos], array[pos]
pos = parentpos
cdef int heapq_init(HeapQueue* heap) nogil except 0:
heap.array = <Callback**>PyMem_RawMalloc(
sizeof(Callback*) * HEAP_BLOCK_SIZE
)
if heap.array == NULL:
with gil:
raise MemoryError
heap.size = HEAP_BLOCK_SIZE
heap.tail = 0
return 1
cdef void heapq_uninit(HeapQueue* heap) nogil:
cdef:
int i = 0, tail = heap.tail
Callback** array = heap.array
if array == NULL:
return
if i < tail:
with gil:
while i < tail:
Py_DECREF(<object>array[i].handle)
PyMem_RawFree(array)
heap.array = NULL
cdef heapq_push_py(HeapQueue* heap, Handle handle):
cdef Callback* callback = &handle.cb
Py_INCREF(handle)
with nogil:
heapq_push(heap, callback, 1)
cdef int heapq_push(
HeapQueue* heap, Callback* callback, int keep
) nogil except 0:
cdef:
int size = heap.size, tail = heap.tail
Callback** array = heap.array
if tail == size:
size += HEAP_BLOCK_SIZE
array = <Callback**>PyMem_RawRealloc(array, sizeof(Callback*) * size)
if array == NULL:
with gil:
raise MemoryError
heap.size = size
array[tail] = callback
size = heap.tail = tail + 1
if keep:
siftdown(heap, tail, size)
return 1
cdef Handle heapq_pop_py(HeapQueue* heap):
cdef:
Handle handle
Callback* callback
with nogil:
callback = heapq_pop(heap)
if callback == NULL:
return None
else:
handle = <Handle>callback.handle
Py_DECREF(handle)
return handle
cdef Callback* heapq_pop(HeapQueue* heap) nogil:
cdef:
Callback* rv
Callback** array = heap.array
int size = heap.size, tail = heap.tail
if tail == 0:
return NULL
tail = heap.tail = tail - 1
rv = array[tail]
if tail == 0:
if size > HEAP_BLOCK_SIZE:
size = HEAP_BLOCK_SIZE
if PyMem_RawRealloc(array, sizeof(Callback*) * size) != NULL:
heap.size = size
return rv
rv, array[0] = array[0], rv
if tail > 1:
siftup(heap, 0, tail)
if size - tail >= HEAP_BLOCK_SIZE * 2:
size -= HEAP_BLOCK_SIZE
if PyMem_RawRealloc(array, sizeof(Callback*) * size) != NULL:
heap.size = size
return rv
cdef inline int keep_top_bit(int n) nogil:
cdef int i = 0
while n > 1:
n >>= 1
i += 1
return n << i
cdef inline void heapq_cache_friendly_heapify(HeapQueue* heap, int tail) nogil:
cdef:
int m = tail >> 1, mhalf = m >> 1
int leftmost = keep_top_bit(m + 1) - 1
int i = leftmost - 1, j
while i >= mhalf:
j = i
while True:
siftup(heap, j, tail)
if not (j & 1):
break
j >>= 1
i -= 1
i = m - 1
while i >= leftmost:
j = i
while True:
siftup(heap, j, tail)
if not (j & 1):
break
j >>= 1
i -= 1
cdef void heapq_heapify(HeapQueue* heap) nogil:
cdef int tail = heap.tail, i = (tail >> 1) - 1
if tail > 2500:
heapq_cache_friendly_heapify(heap, tail)
else:
while i >= 0:
siftup(heap, i, tail)
i -= 1

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -7,4 +7,3 @@
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

View file

@ -0,0 +1,27 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from "<stdatomic.h>" nogil:
ctypedef enum MemoryOrder "memory_order":
relaxed "memory_order_relaxed"
acquire "memory_order_acquire"
release "memory_order_release"
seq_cst "memory_order_seq_cst"
ctypedef unsigned uint "atomic_uint"
unsigned load_explicit "atomic_load_explicit" (
uint* object, MemoryOrder order
)
void store_explicit "atomic_store_explicit" (
uint* object, unsigned desired, MemoryOrder order
)
void thread_fence "atomic_thread_fence" (MemoryOrder order)

View file

@ -0,0 +1,79 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from "sys/syscall.h" nogil:
int SYS_io_uring_setup
int SYS_io_uring_enter
int SYS_io_uring_register
cdef extern from "unistd.h" nogil:
int syscall(int number, ...)
cdef extern from "signal.h" nogil:
int _NSIG
cdef extern from "sys/socket.h" nogil:
enum:
SOCK_DGRAM
SOCK_STREAM
ctypedef int socklen_t
ctypedef int sa_family_t
ctypedef int in_port_t
int SOL_TLS
struct sockaddr:
sa_family_t sa_family
struct msghdr:
void* msg_name # Optional address
socklen_t msg_namelen # Size of address
iovec* msg_iov # Scatter/gather array
size_t msg_iovlen # Number of elements in msg_iov
void* msg_control # ancillary data, see below
size_t msg_controllen # ancillary data buffer len
int msg_flags # flags on received message
struct cmsghdr:
socklen_t cmsg_len # data byte count, including header
int cmsg_level # originating protocol
int cmsg_type # protocol-specific type
size_t CMSG_LEN(size_t length)
cmsghdr* CMSG_FIRSTHDR(msghdr* msgh)
unsigned char* CMSG_DATA(cmsghdr* cmsg)
size_t CMSG_SPACE(size_t length)
int socket(int domain, int type, int protocol)
int setsockopt(
int socket,
int level,
int option_name,
const void* option_value,
socklen_t option_len,
)
int bind(int sockfd, const sockaddr* addr, socklen_t addrlen)
cdef extern from "arpa/inet.h" nogil:
int inet_pton(int af, char* src, void* dst)
int htons(short p)
cdef extern from "sys/uio.h" nogil:
struct iovec:
void* iov_base
size_t iov_len
cdef extern from "<errno.h>" nogil:
enum:
ECANCELED

View file

@ -0,0 +1,135 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from "linux/fs.h" nogil:
ctypedef int __kernel_rwf_t
cdef extern from "linux/types.h" nogil:
ctypedef int __u8
ctypedef int __u16
ctypedef int __u64
ctypedef int __u32
ctypedef int __s32
ctypedef int __kernel_time64_t
cdef extern from "linux/time_types.h" nogil:
struct __kernel_timespec:
__kernel_time64_t tv_sec
long long tv_nsec
cdef extern from "linux/tcp.h" nogil:
int TCP_ULP
cdef extern from "linux/tls.h" nogil:
int TLS_GET_RECORD_TYPE
__u16 TLS_CIPHER_AES_GCM_256
int TLS_CIPHER_AES_GCM_256_IV_SIZE
int TLS_CIPHER_AES_GCM_256_SALT_SIZE
int TLS_CIPHER_AES_GCM_256_KEY_SIZE
int TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE
int TLS_TX
int TLS_RX
struct tls_crypto_info:
__u16 version
__u16 cipher_type
struct tls12_crypto_info_aes_gcm_256:
tls_crypto_info info
unsigned char* iv
unsigned char* key
unsigned char* salt
unsigned char* rec_seq
cdef extern from "linux/io_uring.h" nogil:
unsigned IORING_SETUP_SQPOLL
unsigned IORING_SETUP_SQ_AFF
unsigned IORING_ENTER_GETEVENTS
unsigned IORING_ENTER_SQ_WAKEUP
unsigned IORING_ENTER_EXT_ARG
unsigned IORING_SQ_NEED_WAKEUP
unsigned IORING_SQ_CQ_OVERFLOW
unsigned long long IORING_OFF_SQ_RING
unsigned long long IORING_OFF_SQES
unsigned IORING_TIMEOUT_ABS
unsigned IOSQE_IO_LINK
enum Operation:
IORING_OP_NOP
IORING_OP_CONNECT
IORING_OP_SEND
IORING_OP_SENDMSG
IORING_OP_RECV
IORING_OP_RECVMSG
IORING_OP_OPENAT
IORING_OP_READ
IORING_OP_CLOSE
struct io_sqring_offsets:
__u32 head
__u32 tail
__u32 ring_mask
__u32 ring_entries
__u32 flags
__u32 dropped
__u32 array
struct io_cqring_offsets:
__u32 head
__u32 tail
__u32 ring_mask
__u32 ring_entries
__u32 overflow
__u32 cqes
__u32 flags
struct io_uring_params:
__u32 flags
__u32 sq_thread_cpu
__u32 sq_thread_idle
# written by the kernel:
__u32 sq_entries
__u32 cq_entries
__u32 features
__u32 resv[4]
io_sqring_offsets sq_off
io_cqring_offsets cq_off
struct io_uring_sqe:
__u8 opcode # type of operation for this sqe
__s32 fd # file descriptor to do IO on
__u64 off # offset into file
__u64 addr # pointer to buffer or iovecs
__u32 len # buffer size or number of iovecs
__u64 user_data # data to be passed back at completion time
__u8 flags # IOSQE_ flags
__u32 open_flags
struct io_uring_cqe:
__u64 user_data # data to be passed back at completion time
__s32 res # result code for this event
struct io_uring_getevents_arg:
__u64 sigmask
__u32 sigmask_sz
__u64 ts

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -7,4 +7,3 @@
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

View file

@ -0,0 +1,94 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from "openssl/bio.h" nogil:
enum BIOCtrl:
BIO_CTRL_RESET # 1 opt - rewind/zero etc
BIO_CTRL_EOF # 2 opt - are we at the eof
BIO_CTRL_INFO # 3 opt - extra tit-bits
BIO_CTRL_SET # 4 man - set the 'IO' type
BIO_CTRL_GET # 5 man - get the 'IO' type
BIO_CTRL_PUSH # 6 opt - internal, used to signify change
BIO_CTRL_POP # 7 opt - internal, used to signify change
BIO_CTRL_GET_CLOSE # 8 man - set the 'close' on free
BIO_CTRL_SET_CLOSE # 9 man - set the 'close' on free
BIO_CTRL_PENDING # 10 opt - is their more data buffered
BIO_CTRL_FLUSH # 11 opt - 'flush' buffered output
BIO_CTRL_DUP # 12 man - extra stuff for 'duped' BIO
BIO_CTRL_WPENDING # 13 opt - number of bytes still to write
BIO_CTRL_SET_CALLBACK # 14 opt - set callback function
BIO_CTRL_GET_CALLBACK # 15 opt - set callback function
ctypedef struct Method "BIO_METHOD":
pass
ctypedef struct BIO:
pass
int get_new_index "BIO_get_new_index" ()
Method* meth_new "BIO_meth_new" (int type, const char* name)
int meth_set_write_ex "BIO_meth_set_write_ex" (
Method* biom,
int (*bwrite)(BIO*, const char*, size_t, size_t*),
)
int meth_set_write "BIO_meth_set_write" (
Method* biom,
int (*write)(BIO*, const char*, int),
)
int meth_set_read_ex "BIO_meth_set_read_ex" (
Method* biom,
int (*bread)(BIO*, char*, size_t, size_t*),
)
int meth_set_read "BIO_meth_set_read"(
Method* biom,
int (*read)(BIO*, char*, int),
)
int meth_set_ctrl "BIO_meth_set_ctrl" (
Method* biom, long (*ctrl)(BIO*, int, long, void*)
)
int meth_set_create "BIO_meth_set_create" (
Method* biom,
int (*create)(BIO*),
)
int meth_set_destroy "BIO_meth_set_destroy" (
Method* biom,
int (*destroy)(BIO*),
)
ctypedef int info_cb "BIO_info_cb" (BIO*, int, int)
int meth_set_callback_ctrl "BIO_meth_set_callback_ctrl" (
Method* biom,
long (*callback_ctrl)(BIO*, int, info_cb*),
)
BIO* new "BIO_new" (const Method* type)
int up_ref "BIO_up_ref" (BIO* a)
int free "BIO_free" (BIO* a)
void set_data "BIO_set_data" (BIO* a, void* ptr)
void* get_data "BIO_get_data" (BIO* a)
void set_init "BIO_set_init" (BIO* a, int init)
void set_shutdown "BIO_set_shutdown" (BIO* a, int shut)
void set_retry_read "BIO_set_retry_read" (BIO* b)
void set_retry_write "BIO_set_retry_write" (BIO* b)
void clear_retry_flags "BIO_clear_retry_flags" (BIO *b)
cdef int FLAGS_IN_EOF "BIO_FLAGS_IN_EOF"
int test_flags "BIO_test_flags" (BIO* b, int flags)
void set_flags "BIO_set_flags" (BIO *b, int flags)

View file

@ -0,0 +1,14 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from "openssl/err.h" nogil:
unsigned long get_error "ERR_get_error" ()
const char* reason_error_string "ERR_reason_error_string" (unsigned long e)

View file

@ -0,0 +1,43 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
from .. cimport linux
cdef extern from "openssl/ssl.h" nogil:
ctypedef struct SSL:
pass
int SSL3_RT_APPLICATION_DATA
int OP_ENABLE_KTLS "SSL_OP_ENABLE_KTLS"
int set_options "SSL_set_options" (SSL* ssl, int options)
long SSL_MODE_RELEASE_BUFFERS
long SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER
long clear_mode "SSL_clear_mode" (SSL* ssl, long mode)
int free_buffers "SSL_free_buffers" (SSL *ssl)
cdef extern from *:
"""
typedef struct {
union {
struct tls12_crypto_info_aes_gcm_128 gcm128;
struct tls12_crypto_info_aes_gcm_256 gcm256;
struct tls12_crypto_info_aes_ccm_128 ccm128;
struct tls12_crypto_info_chacha20_poly1305 chacha20poly1305;
};
size_t tls_crypto_info_len;
} ktls_crypto_info_t;
"""
ctypedef struct ktls_crypto_info_t:
size_t tls_crypto_info_len

View file

@ -0,0 +1,34 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
from .openssl.ssl cimport SSL
from .openssl.bio cimport BIO
cdef extern from *:
"""
typedef struct {
PyObject_HEAD
PyObject *Socket; /* weakref to socket on which we're layered */
SSL *ssl;
} PySSLSocket;
typedef struct {
PyObject_HEAD
BIO *bio;
int eof_written;
} PySSLMemoryBIO;
"""
ctypedef struct PySSLSocket:
SSL* ssl
ctypedef struct PySSLMemoryBIO:
BIO* bio

56
src/kloop/loop.pxd Normal file
View file

@ -0,0 +1,56 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
from cpython cimport PyErr_SetFromErrno
from cpython cimport PyMem_RawMalloc, PyMem_RawFree, PyMem_RawRealloc
from cpython cimport PyObject, Py_INCREF, Py_DECREF
from libc cimport errno, string
from posix cimport mman, unistd, time
from posix.types cimport mode_t
from .includes cimport atomic, libc, linux
include "./handle.pxd"
include "./queue.pxd"
include "./heapq.pxd"
include "./uring.pxd"
include "./tcp.pxd"
include "./udp.pxd"
include "./fileio.pxd"
include "./resolver.pxd"
cdef struct Loop:
bint stopping
Ring ring
HeapQueue scheduled
Queue ready
int timer_cancelled_count
PyObject* loop
CResolver resolver
cdef class KLoopImpl:
cdef:
bint closed
object thread_id
Loop loop
Resolver resolver
cpdef create_future(self)
cdef inline check_closed(self)
cdef inline bint _is_running(self)
cdef inline check_running(self)
cdef inline Handle _call_soon(self, callback, args, context)
cdef inline _add_callback(self, Handle handle)
cdef inline TimerHandle _call_at(
self, long long when, callback, args, context
)

560
src/kloop/loop.pyx Normal file
View file

@ -0,0 +1,560 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
import time as py_time
import asyncio
import contextvars
import functools
import inspect
import os
import reprlib
import threading
import traceback
cdef asyncio_isfuture = asyncio.isfuture
cdef asyncio_ensure_future = asyncio.ensure_future
cdef asyncio_set_running_loop = asyncio._set_running_loop
cdef asyncio_get_running_loop = asyncio._get_running_loop
cdef asyncio_Task = asyncio.Task
cdef asyncio_Future = asyncio.Future
cdef logger = asyncio.log.logger
cdef long long SECOND_NS = 1_000_000_000
cdef long long MAX_SELECT_TIMEOUT = 24 * 3600 * SECOND_NS
# Minimum number of scheduled timer handles before cleanup of
# cancelled handles is performed.
cdef int MIN_SCHEDULED_TIMER_HANDLES = 100
# Maximum ratio of cancelled handles is performed of scheduled timer handles
# that are cancelled before cleanup
cdef int MAX_CANCELLED_TIMER_HANDLES_RATIO = 2
include "handle.pyx"
include "queue.pyx"
include "heapq.pyx"
include "uring.pyx"
include "tcp.pyx"
include "udp.pyx"
include "fileio.pyx"
include "resolver.pyx"
cdef long long monotonic_ns() nogil except -1:
cdef:
long long rv
time.timespec ts
if time.clock_gettime(time.CLOCK_MONOTONIC, &ts):
with gil:
PyErr_SetFromErrno(OSError)
return -1
rv = ts.tv_sec * SECOND_NS
return rv + ts.tv_nsec
cdef int loop_init(
Loop* loop, linux.__u32 depth, linux.io_uring_params* params
) nogil except 0:
if not queue_init(&loop.ready):
return 0
if not heapq_init(&loop.scheduled):
queue_uninit(&loop.ready)
return 0
if not ring_init(&loop.ring, depth, params):
queue_uninit(&loop.ready)
heapq_uninit(&loop.scheduled)
return 0
return 1
cdef int loop_uninit(Loop* loop) nogil except 0:
heapq_uninit(&loop.scheduled)
queue_uninit(&loop.ready)
return ring_uninit(&loop.ring)
cdef int loop_run_forever(Loop* loop) nogil except 0:
cdef:
Ring* ring = &loop.ring
Queue* ready = &loop.ready
HeapQueue* scheduled = &loop.scheduled
while True:
if not loop_run_once(loop, ring, ready, scheduled):
return 0
if loop.stopping:
break
return 1
cdef inline int filter_cancelled_calls(Loop* loop) nogil except 0:
cdef:
HeapQueue* scheduled = &loop.scheduled
HeapQueue heap, drop
Callback** array = scheduled.array
Callback* callback
int i = 0, size = scheduled.tail
if (
MIN_SCHEDULED_TIMER_HANDLES < size <
loop.timer_cancelled_count * MAX_CANCELLED_TIMER_HANDLES_RATIO
):
# Remove delayed calls that were cancelled if their number
# is too high
if not heapq_init(&drop):
return 0
if not heapq_init(&heap):
heapq_uninit(&drop)
return 0
while i < size:
callback = array[i]
if callback.mask & CANCELLED_MASK:
callback.mask &= ~SCHEDULED_MASK
if not heapq_push(&drop, callback, 0):
heap.tail = 0
heapq_uninit(&heap)
drop.tail = 0
heapq_uninit(&drop)
return 0
elif not heapq_push(&heap, callback, 0):
heap.tail = 0
heapq_uninit(&heap)
drop.tail = 0
heapq_uninit(&drop)
return 0
heapq_heapify(&heap)
heap, scheduled[0] = scheduled[0], heap
heap.tail = 0
heapq_uninit(&heap)
heapq_uninit(&drop)
elif array[0].mask & CANCELLED_MASK:
if not heapq_init(&drop):
return 0
while size:
callback = heapq_pop(scheduled)
if callback.mask & CANCELLED_MASK:
loop.timer_cancelled_count -= 1
callback.mask &= ~SCHEDULED_MASK
if not heapq_push(&drop, callback, 0):
with gil:
Py_DECREF(<object>callback.handle)
heapq_uninit(&drop)
return 0
if not array[0].mask & CANCELLED_MASK:
break
size -= 1
heapq_uninit(&drop)
return 1
cdef loop_run_ready(Queue* ready, int ntodo):
cdef Handle handle
while ntodo:
handle = queue_pop_py(ready)
if not handle.cb.mask & CANCELLED_MASK:
handle.run()
ntodo -= 1
handle = None
cdef inline int loop_run_once(
Loop* loop, Ring* ring, Queue* ready, HeapQueue* scheduled
) nogil except 0:
cdef:
Callback* callback
long long timeout = -1, now
int nready
RingCallback* cb = NULL
if scheduled.tail:
if not filter_cancelled_calls(loop):
return 0
if ready.head >= 0 or loop.stopping:
timeout = 0
elif scheduled.tail:
timeout = min(
max(0, scheduled.array[0].when - monotonic_ns()),
MAX_SELECT_TIMEOUT,
)
nready = ring_select(ring, timeout)
if nready < 0:
return 0
while nready:
ring_cq_pop(&ring.cq, &cb)
if cb != NULL and not cb.callback(cb):
return 0
nready -= 1
now = monotonic_ns() + 1
while scheduled.tail and scheduled.array[0].when < now:
callback = heapq_pop(scheduled)
callback.mask &= ~SCHEDULED_MASK
if not queue_push(ready, callback):
if not heapq_push(scheduled, callback, 1):
with gil:
Py_DECREF(<object>callback.handle)
return 0
if ready.head >= 0:
with gil:
loop_run_ready(ready, queue_size(ready))
return 1
class KLoopPolicy(asyncio.events.BaseDefaultEventLoopPolicy):
__slots__ = ("_selector_args",)
def __init__(
self, queue_depth=128, sq_thread_idle=2000, sq_thread_cpu=None
):
super().__init__()
self._selector_args = (queue_depth, sq_thread_idle, sq_thread_cpu)
def _loop_factory(self):
return KLoop(*self._selector_args)
# Child processes handling (Unix only).
def get_child_watcher(self):
raise NotImplementedError
def set_child_watcher(self, watcher):
raise NotImplementedError
cdef class KLoopImpl:
def __init__(self, queue_depth, sq_thread_idle, sq_thread_cpu):
cdef:
linux.io_uring_params params
linux.__u32 depth
string.memset(&params, 0, sizeof(params))
params.flags = linux.IORING_SETUP_SQPOLL
params.sq_thread_idle = sq_thread_idle
if sq_thread_cpu is not None:
params.sq_thread_cpu = sq_thread_cpu
params.flags |= linux.IORING_SETUP_SQ_AFF
depth = queue_depth
self.loop.loop = <PyObject*>self
with nogil:
loop_init(&self.loop, depth, &params)
self.resolver = Resolver.new(self)
self.closed = False
self.thread_id = None
def __dealloc__(self):
with nogil:
loop_uninit(&self.loop)
cdef inline check_closed(self):
if self.closed:
raise RuntimeError('Event loop is closed')
cdef inline bint _is_running(self):
return self.thread_id is not None
cdef inline check_running(self):
if self._is_running():
raise RuntimeError('This event loop is already running')
if asyncio_get_running_loop() is not None:
raise RuntimeError(
'Cannot run the event loop while another loop is running')
def run_forever(self):
"""Run until stop() is called."""
self.check_closed()
self.check_running()
# self._set_coroutine_origin_tracking(self._debug)
self.thread_id = threading.get_ident()
# old_agen_hooks = sys.get_asyncgen_hooks()
# sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook,
# finalizer=self._asyncgen_finalizer_hook)
try:
asyncio_set_running_loop(self)
with nogil:
loop_run_forever(&self.loop)
finally:
self.loop.stopping = 0
self.thread_id = None
asyncio_set_running_loop(None)
# self._set_coroutine_origin_tracking(False)
# sys.set_asyncgen_hooks(*old_agen_hooks)
def run_until_complete(self, future):
self.check_closed()
self.check_running()
new_task = not asyncio_isfuture(future)
future = asyncio_ensure_future(future, loop=self)
if new_task:
# An exception is raised if the future didn't complete, so there
# is no need to log the "destroy pending task" message
future._log_destroy_pending = False
future.add_done_callback(_run_until_complete_cb)
try:
self.run_forever()
except:
if new_task and future.done() and not future.cancelled():
# The coroutine raised a BaseException. Consume the exception
# to not log a warning, the caller doesn't have access to the
# local task.
future.exception()
raise
finally:
future.remove_done_callback(_run_until_complete_cb)
if not future.done():
raise RuntimeError('Event loop stopped before Future completed.')
return future.result()
def create_task(self, coro, *, name=None):
self.check_closed()
# if self._task_factory is None:
task = asyncio_Task(coro, loop=self, name=name)
if task._source_traceback:
del task._source_traceback[-1]
# else:
# task = self._task_factory(self, coro)
# tasks._set_task_name(task, name)
return task
def stop(self):
self.loop.stopping = 1
def close(self):
if self.is_running():
raise RuntimeError("Cannot close a running event loop")
if self.closed:
return
# if self._debug:
# logger.debug("Close %r", self)
self.closed = True
# self.ready.clear()
# self._scheduled.clear()
# self._executor_shutdown_called = True
# executor = self._default_executor
# if executor is not None:
# self._default_executor = None
# executor.shutdown(wait=False)
def fileno(self):
return self.loop.ring.ring_fd
def is_running(self):
return self._is_running()
def get_debug(self):
return False
def call_soon(self, callback, *args, context=None):
cdef Handle handle
self.check_closed()
# if self._debug:
# self._check_thread()
# self._check_callback(callback, 'call_soon')
handle = self._call_soon(callback, args, context)
if handle.source_traceback:
del handle.source_traceback[-1]
return handle
def time(self):
return (<float>monotonic_ns()) / SECOND_NS
def call_later(self, delay, callback, *args, context=None):
cdef long long when = monotonic_ns()
when += delay * SECOND_NS
timer = self._call_at(when, callback, args, context)
if timer.source_traceback:
del timer.source_traceback[-1]
return timer
def call_at(self, when, callback, *args, context=None):
timer = self._call_at(when * SECOND_NS, callback, args, context)
if timer.source_traceback:
del timer.source_traceback[-1]
return timer
cdef inline TimerHandle _call_at(
self, long long when, callback, args, context
):
cdef TimerHandle timer
self.check_closed()
# if self._debug:
# self._check_thread()
# self._check_callback(callback, 'call_at')
timer = TimerHandle(when, callback, args, self, context)
heapq_push_py(&self.loop.scheduled, timer)
# else:
# heapq_heappush(self.heapq)
timer.cb.mask |= SCHEDULED_MASK
return timer
cdef inline Handle _call_soon(self, callback, args, context):
cdef Handle handle = Handle(callback, args, self, context)
self._add_callback(handle)
return handle
cdef inline _add_callback(self, Handle handle):
queue_push_py(&self.loop.ready, handle)
def default_exception_handler(self, context):
message = context.get('message')
if not message:
message = 'Unhandled exception in event loop'
exception = context.get('exception')
if exception is not None:
exc_info = (type(exception), exception, exception.__traceback__)
else:
exc_info = False
# if ('source_traceback' not in context and
# self._current_handle is not None and
# self._current_handle._source_traceback):
# context['handle_traceback'] = \
# self._current_handle._source_traceback
log_lines = [message]
for key in sorted(context):
if key in {'message', 'exception'}:
continue
value = context[key]
if key == 'source_traceback':
tb = ''.join(traceback.format_list(value))
value = 'Object created at (most recent call last):\n'
value += tb.rstrip()
elif key == 'handle_traceback':
tb = ''.join(traceback.format_list(value))
value = 'Handle created at (most recent call last):\n'
value += tb.rstrip()
else:
value = repr(value)
log_lines.append(f'{key}: {value}')
logger.error('\n'.join(log_lines), exc_info=exc_info)
def call_exception_handler(self, context):
# if self._exception_handler is None:
try:
self.default_exception_handler(context)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException:
# Second protection layer for unexpected errors
# in the default implementation, as well as for subclassed
# event loops with overloaded "default_exception_handler".
logger.error('Exception in default exception handler',
exc_info=True)
# else:
# try:
# self._exception_handler(self, context)
# except (SystemExit, KeyboardInterrupt):
# raise
# except BaseException as exc:
# # Exception in the user set custom exception handler.
# try:
# # Let's try default handler.
# self.default_exception_handler({
# 'message': 'Unhandled error in exception handler',
# 'exception': exc,
# 'context': context,
# })
# except (SystemExit, KeyboardInterrupt):
# raise
# except BaseException:
# # Guard 'default_exception_handler' in case it is
# # overloaded.
# logger.error('Exception in default exception handler '
# 'while handling an unexpected error '
# 'in custom exception handler',
# exc_info=True)
async def shutdown_asyncgens(self):
pass
async def shutdown_default_executor(self):
pass
cpdef create_future(self):
return asyncio_Future(loop=self)
def _timer_handle_cancelled(self, handle):
pass
async def create_connection(
self,
protocol_factory,
host=None,
port=None,
*,
ssl=None,
family=0,
proto=0,
flags=0,
sock=None,
local_addr=None,
server_hostname=None,
ssl_handshake_timeout=None,
happy_eyeballs_delay=None,
interleave=None,
):
cdef:
int fd
if ssl is False:
ssl = None
elif ssl is not None:
from . import tls
if ssl is True:
import ssl as ssl_mod
ssl = ssl_mod.create_default_context()
fd = await tcp_connect(self, host, port)
protocol = protocol_factory()
if ssl is not None:
waiter = self.create_future()
transport = tls.TLSTransport.new(fd, protocol, self, ssl, waiter=waiter)
await waiter
else:
transport = TCPTransport.new(fd, protocol, self)
return transport, protocol
class KLoop(KLoopImpl, asyncio.AbstractEventLoop):
pass
def _run_until_complete_cb(fut):
if not fut.cancelled():
exc = fut.exception()
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
# Issue #22429: run_forever() already finished, no need to
# stop it.
return
_get_loop(fut).stop()
def _get_loop(fut):
# Tries to call Future.get_loop() if it's available.
# Otherwise fallbacks to using the old '_loop' property.
try:
get_loop = fut.get_loop
except AttributeError:
pass
else:
return get_loop()
return fut._loop

16
src/kloop/queue.pxd Normal file
View file

@ -0,0 +1,16 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct Queue:
Callback** array
int size
int head
int tail

165
src/kloop/queue.pyx Normal file
View file

@ -0,0 +1,165 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef int QUEUE_BLOCK_SIZE = 1024
cdef int queue_init(Queue* queue) nogil except 0:
queue.array = <Callback**>PyMem_RawMalloc(
sizeof(Callback*) * QUEUE_BLOCK_SIZE
)
if queue.array == NULL:
with gil:
raise MemoryError
queue.head = -1
queue.tail = 0
queue.size = QUEUE_BLOCK_SIZE
return 1
cdef void queue_uninit(Queue* queue) nogil:
cdef:
int i = queue.head, size = queue.size, tail = queue.tail
Callback** array = queue.array
if array == NULL:
return
if i >= 0:
with gil:
while True:
Py_DECREF(<object>array[i].handle)
i = (i + 1) % size
if i == tail:
break
PyMem_RawFree(array)
queue.array = NULL
cdef queue_push_py(Queue* queue, Handle handle):
cdef Callback* callback = &handle.cb
Py_INCREF(handle)
with nogil:
queue_push(queue, callback)
cdef int queue_push(Queue* queue, Callback* callback) nogil except 0:
cdef:
Callback** orig = queue.array
Callback** array = orig
int size = queue.size, head = queue.head, tail = queue.tail
if head == tail:
if head == 0:
tail = size
size += QUEUE_BLOCK_SIZE
array = <Callback**>PyMem_RawRealloc(
orig, sizeof(Callback*) * size
)
if array == NULL:
with gil:
raise MemoryError
else:
tail = size + QUEUE_BLOCK_SIZE
array = <Callback**>PyMem_RawMalloc(sizeof(Callback*) * tail)
if array == NULL:
with gil:
raise MemoryError
queue.array = array
string.memcpy(
array, orig + head, sizeof(Callback*) * (size - head)
)
string.memcpy(array + size - head, orig, sizeof(Callback*) * head)
size, tail = tail, size
queue.head = head = 0
PyMem_RawFree(orig)
queue.size = size
elif head < 0:
queue.head = head = 0
array[tail] = callback
queue.tail = (tail + 1) % size
return 1
cdef Handle queue_pop_py(Queue* queue):
cdef:
Handle handle
Callback* callback
with nogil:
callback = queue_pop(queue)
if callback == NULL:
return None
else:
handle = <Handle>callback.handle
Py_DECREF(handle)
return handle
cdef Callback* queue_pop(Queue* queue) nogil:
cdef:
int size = queue.size, head = queue.head, tail = queue.tail
Callback* rv
Callback** orig = queue.array
Callback** array = orig
if head < 0:
return NULL
rv = array[head]
queue.head = head = (head + 1) % size
if head == tail:
queue.head = -1
queue.tail = 0
if size > QUEUE_BLOCK_SIZE:
size = QUEUE_BLOCK_SIZE
if PyMem_RawRealloc(
array, sizeof(Callback*) * size
) != NULL:
queue.size = size
elif (head - tail) % size >= QUEUE_BLOCK_SIZE * 2:
if head < tail:
size -= QUEUE_BLOCK_SIZE
if tail > size:
tail -= head
string.memmove(array, array + head, sizeof(Callback*) * tail)
queue.tail = tail
queue.head = 0
if PyMem_RawRealloc(
array, sizeof(Callback*) * size
) != NULL:
queue.size = size
queue.tail = tail % size
else:
array = <Callback**>PyMem_RawMalloc(
sizeof(Callback*) * (size - QUEUE_BLOCK_SIZE)
)
if array != NULL:
string.memcpy(
array, orig + head, sizeof(Callback*) * (size - head)
)
string.memcpy(
array + size - head, orig, sizeof(Callback*) * tail
)
queue.array = array
queue.head = 0
queue.tail = (tail - head) % size
queue.size = size - QUEUE_BLOCK_SIZE
PyMem_RawFree(orig)
return rv
cdef int queue_size(Queue* queue) nogil:
cdef int size = queue.size, head = queue.head, tail = queue.tail
if head < 0:
return 0
elif head == tail:
return size
else:
return (tail - head) % size

75
src/kloop/resolver.pxd Normal file
View file

@ -0,0 +1,75 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern from * nogil:
int resolver_init(
CResolver* resolver,
char* resolv_conf_data,
size_t resolv_conf_data_size,
char* hosts_conf_data,
size_t hosts_conf_data_size,
)
int resolver_lookup_ip(
void* resolver,
void* resolve,
char* host,
size_t length,
libc.in_port_t port,
)
void resolver_run_until_stalled(void* rust_resolver)
void waker_wake(void* waker)
void waker_forget(void* waker)
cdef struct CResolver:
Loop* loop
Callback* cb
FileReader resolv_conf
FileReader hosts_conf
int res
void* rust_resolver
cdef class Resolver:
cdef:
CResolver resolver
KLoopImpl loop
Handle handle
object waiter
bint initialized
@staticmethod
cdef Resolver new(KLoopImpl loop)
cdef init_cb(self)
cdef err_cb(self, exc)
cdef struct CResolve:
CResolver* resolver
libc.sockaddr* result
size_t result_len, result_size
Callback* cb
int res
char* host
size_t host_len
libc.in_port_t port
cdef class Resolve:
cdef:
CResolve r
Handle handle
object waiter
object host
@staticmethod
cdef new(Resolver resolver, host, port)
cdef resolve_cb(self)

179
src/kloop/resolver.pyx Normal file
View file

@ -0,0 +1,179 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef const char* RESOLV_CONF = "/etc/resolv.conf"
cdef const char* HOSTS_CONF = "/etc/hosts"
cdef size_t SOCKADDR_CHUNK_SIZE = 4
cdef int resolve_cb(RingCallback* cb) nogil except 0:
cdef:
CResolver* r = <CResolver*>cb.data
int rv = 1
return rv
cdef int resolver_read_file_cb(RingCallback* cb) nogil except 0:
cdef:
CResolver* r = <CResolver*>cb.data
int rv = 1
void* ptr
if r.hosts_conf.done_cb.res == 0 or r.resolv_conf.done_cb.res == 0:
if cb.res < 0:
r.resolv_conf.cancelled = r.hosts_conf.cancelled = 1
else:
r.res = min(r.hosts_conf.done_cb.res, r.resolv_conf.done_cb.res)
if r.res > 0:
r.res = resolver_init(
r,
r.resolv_conf.data,
r.resolv_conf.offset,
r.hosts_conf.data,
r.hosts_conf.offset,
)
rv = queue_push(&r.loop.ready, r.cb)
if not file_reader_done(&r.resolv_conf):
# TODO: fd not closed?
pass
if not file_reader_done(&r.hosts_conf):
# TODO: fd not closed?
pass
return rv
cdef extern libc.sockaddr* resolve_prep_addr(CResolve* r) nogil:
cdef size_t l = r.result_len, size = r.result_size
if l == size:
size += SOCKADDR_CHUNK_SIZE
if PyMem_RawRealloc(r.result, sizeof(libc.sockaddr) * size) == NULL:
return NULL
r.result_size = size
r.result_len = l + 1
return r.result + l
cdef extern int resolve_done_cb(CResolve* r) nogil:
return queue_push(&r.resolver.loop.ready, r.cb)
cdef extern void resolver_set(CResolver* resolver, void* rust_resolver) nogil:
resolver.rust_resolver = rust_resolver
cdef class Resolver:
@staticmethod
cdef Resolver new(KLoopImpl loop):
cdef:
Resolver rv = Resolver.__new__(Resolver)
CResolver* r = &rv.resolver
rv.loop = loop
r.loop = &loop.loop
r.resolv_conf.done_cb.callback = resolver_read_file_cb
r.resolv_conf.done_cb.data = r
r.hosts_conf.done_cb.callback = resolver_read_file_cb
r.hosts_conf.done_cb.data = r
return rv
async def ensure_initialized(self):
cdef CResolver* r
if self.initialized:
return
waiter = self.waiter
if waiter is None:
r = &self.resolver
waiter = self.waiter = self.loop.create_future()
if not file_reader_start(&r.resolv_conf, r.loop, RESOLV_CONF):
self.err_cb(ValueError("Submission queue is full!"))
elif not file_reader_start(&r.hosts_conf, r.loop, HOSTS_CONF):
r.resolv_conf.cancelled = 1
self.err_cb(ValueError("Submission queue is full!"))
else:
self.handle = Handle(self.init_cb, (self,), self.loop, None)
r.cb = &self.handle.cb
await waiter
async def reload_config(self, *, force=False):
if self.initialized:
waiter = self.waiter
if waiter is None:
waiter = self.waiter = self.loop.create_future()
self.err_cb(NotImplementedError())
await waiter
else:
await self.ensure_initialized()
cdef init_cb(self):
cdef int res = self.resolver.res
if self.waiter.done():
return
if res < 0:
try:
errno.errno = -res
PyErr_SetFromErrno(IOError)
except IOError as e:
self.waiter.set_exception(e)
else:
self.waiter.set_result(None)
cdef err_cb(self, exc):
waiter, self.waiter = self.waiter, None
if waiter is not None:
if not waiter.done():
waiter.set_exception(exc)
async def lookup_ip(self, host, port):
await self.ensure_initialized()
return await Resolve.new(self, host, port)
cdef class Resolve:
@staticmethod
cdef new(Resolver resolver, host, port):
cdef:
Resolve rv = Resolve.__new__(Resolve)
CResolve* r = &rv.r
rv.host = host.encode("utf-8")
rv.waiter = resolver.loop.create_future()
r.resolver = &resolver.resolver
r.host = <char*>rv.host
r.host_len = len(rv.host)
r.port = port
r.result = <libc.sockaddr*>PyMem_RawMalloc(
sizeof(libc.sockaddr) * SOCKADDR_CHUNK_SIZE
)
if r.result == NULL:
raise MemoryError
r.result_size = SOCKADDR_CHUNK_SIZE
rv.handle = Handle(rv.resolve_cb, (rv,), resolver.loop, None)
r.cb = &rv.handle.cb
return rv
def __await__(self):
cdef CResolve* r = &self.r
resolver_lookup_ip(r.resolver.rust_resolver, r, r.host, r.host_len, r.port)
return self.waiter.__await__()
def __dealloc__(self):
cdef CResolve* r = &self.r
r.host = NULL
r.host_len = 0
if r.result != NULL:
PyMem_RawFree(r.result)
r.result = NULL
r.result_size = 0
cdef resolve_cb(self):
self.waiter.set_result(self)

25
src/kloop/tcp.pxd Normal file
View file

@ -0,0 +1,25 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct TCPConnect:
RingCallback ring_cb
Loop* loop
Callback* cb
cdef class TCPTransport:
cdef:
KLoopImpl loop
int fd
object protocol
@staticmethod
cdef TCPTransport new(int fd, object protocol, KLoopImpl loop)

78
src/kloop/tcp.pyx Normal file
View file

@ -0,0 +1,78 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
async def tcp_connect(KLoopImpl loop, host, port):
cdef:
Resolve resolve
TCPConnect connector
int fd, res
libc.sockaddr * addr
Handle handle
size_t i
resolve = await loop.resolver.lookup_ip(host, port)
if not resolve.r.result_len:
raise RuntimeError(f"Cannot resolve host: {host!r}")
connector.loop = &loop.loop
connector.ring_cb.callback = tcp_connect_cb
connector.ring_cb.data = &connector
exceptions = []
for i in range(resolve.r.result_len):
addr = resolve.r.result + i
fd = libc.socket(addr.sa_family, libc.SOCK_STREAM, 0)
if fd == -1:
raise IOError("Cannot create socket")
try:
waiter = loop.create_future()
handle = Handle(waiter.set_result, (None,), loop, None)
connector.cb = &handle.cb
if not ring_sq_submit_connect(
&loop.loop.ring.sq,
fd,
addr,
&connector.ring_cb,
):
raise ValueError("Submission queue is full!")
await waiter
res = abs(connector.ring_cb.res)
if res != 0:
raise IOError(res, string.strerror(res))
return fd
except Exception as e:
os.close(fd)
exceptions.append(e)
raise exceptions[0]
cdef int tcp_connect_cb(RingCallback* cb) nogil except 0:
cdef TCPConnect* connector = <TCPConnect*>cb.data
return queue_push(&connector.loop.ready, connector.cb)
cdef class TCPTransport:
@staticmethod
cdef TCPTransport new(int fd, object protocol, KLoopImpl loop):
cdef TCPTransport rv = TCPTransport.__new__(TCPTransport)
rv.fd = fd
rv.protocol = protocol
rv.loop = loop
loop.call_soon(protocol.connection_made, rv)
return rv
def get_extra_info(self, x):
return None

60
src/kloop/tls.pxd Normal file
View file

@ -0,0 +1,60 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
from cpython cimport PyObject
from .includes cimport libc
from .includes.openssl cimport bio
from .loop cimport KLoopImpl, Loop, RingCallback
cdef struct Proxy:
PyObject* transport
libc.iovec send_vec
libc.msghdr send_msg
RingCallback send_callback
libc.iovec recv_vec
libc.msghdr recv_msg
RingCallback recv_callback
unsigned char flags
char* read_buffer
void* cmsg
Loop* loop
int fd
cdef enum State:
UNWRAPPED
HANDSHAKING
WRAPPED
WRAPPED_KTLS
cdef class TLSTransport:
cdef:
KLoopImpl loop
int fd
bio.BIO* bio
object protocol
object sslctx
object sslobj
object waiter
Proxy proxy
State state
object write_buffer
bint sending
cdef do_handshake(self)
cdef do_read(self)
cdef do_read_ktls(self)
cdef write_cb(self, int res)
cdef read_cb(self, int res)

611
src/kloop/tls.pyx Normal file
View file

@ -0,0 +1,611 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
import collections
import socket
import ssl
from cpython cimport PyMem_RawMalloc, PyMem_RawFree
from libc cimport errno, string
from .includes.openssl cimport err, ssl as ssl_h
from .includes cimport pyssl, linux
from .loop cimport ring_sq_submit_sendmsg, ring_sq_submit_recvmsg
cdef int BIO_CTRL_SET_KTLS = 72
cdef int BIO_CTRL_GET_KTLS_SEND = 73
cdef int BIO_CTRL_GET_KTLS_RECV = 76
cdef int FLAGS_KTLS_TX_CTRL_MSG = 0x1000
cdef int FLAGS_KTLS_RX = 0x2000
cdef int FLAGS_KTLS_TX = 0x4000
cdef unsigned char FLAGS_PROXY_SEND_SUBMITTED = 1 << 0
cdef unsigned char FLAGS_PROXY_SEND_COMPLETED = 1 << 1
cdef unsigned char FLAGS_PROXY_SEND_IN_PROXY = 1 << 2
cdef unsigned char FLAGS_PROXY_SEND_ALL = (
FLAGS_PROXY_SEND_SUBMITTED |
FLAGS_PROXY_SEND_COMPLETED |
FLAGS_PROXY_SEND_IN_PROXY
)
cdef unsigned char FLAGS_PROXY_RECV_SUBMITTED = 1 << 4
cdef unsigned char FLAGS_PROXY_RECV_COMPLETED = 1 << 5
cdef unsigned char FLAGS_PROXY_RECV_KTLS = 1 << 6
cdef unsigned char FLAGS_PROXY_RECV_ALL = (
FLAGS_PROXY_RECV_SUBMITTED |
FLAGS_PROXY_RECV_COMPLETED
)
cdef size_t CMSG_SIZE = libc.CMSG_SPACE(sizeof(unsigned char))
cdef inline void reset_msg(libc.msghdr* msg, void* cmsg) nogil:
msg.msg_name = NULL
msg.msg_namelen = 0
msg.msg_flags = 0
if cmsg == NULL:
msg.msg_control = NULL
msg.msg_controllen = 0
else:
msg.msg_control = cmsg
msg.msg_controllen = CMSG_SIZE
cdef object fromOpenSSLError(object err_type):
cdef:
unsigned long e = err.get_error()
const char* msg = err.reason_error_string(e)
if msg == NULL:
return err_type()
else:
return err_type(msg.decode("ISO-8859-1"))
cdef int bio_write_ex(
bio.BIO* b, const char* data, size_t datal, size_t* written
) nogil:
cdef:
Proxy* proxy = <Proxy*>bio.get_data(b)
int res
IF DEBUG:
with gil:
print("bio_write_ex(data=%x, datal=%d)" % (<long long>data, datal))
if proxy.flags & FLAGS_PROXY_SEND_SUBMITTED:
if proxy.send_vec.iov_base != data:
IF DEBUG:
with gil:
print("bio_write_ex() error: concurrent call")
return 0
if proxy.send_vec.iov_len > datal:
IF DEBUG:
with gil:
print("bio_write_ex() error: short rewrite")
return 0
bio.clear_retry_flags(b)
if proxy.flags & FLAGS_PROXY_SEND_COMPLETED:
proxy.flags &= ~FLAGS_PROXY_SEND_ALL
res = proxy.send_callback.res
if res < 0:
IF DEBUG:
with gil:
print("bio_write_ex() error:", -res)
errno.errno = -res
return 0
written[0] = res
IF DEBUG:
with gil:
print("bio_write_ex() written:", res)
print(">>> ", end="")
for i in range(res):
print(
"%02x " % <unsigned char>data[i],
end="" if (i + 1) % 16 or i == res - 1 else "\n>>> ",
)
print()
else:
written[0] = 0
bio.set_retry_write(b)
if not proxy.flags & FLAGS_PROXY_SEND_SUBMITTED:
IF DEBUG:
with gil:
print("bio_write_ex() submit")
proxy.send_vec.iov_base = data
proxy.send_vec.iov_len = datal
reset_msg(&proxy.send_msg, NULL)
if not ring_sq_submit_sendmsg(
&proxy.loop.ring.sq,
proxy.fd,
&proxy.send_msg,
&proxy.send_callback,
):
IF DEBUG:
with gil:
print("bio_write_ex() error: SQ full")
return 0
proxy.flags |= FLAGS_PROXY_SEND_SUBMITTED
return 1
cdef int bio_read_ex(
bio.BIO* b, char* data, size_t datal, size_t* readbytes
) nogil:
cdef:
Proxy* proxy = <Proxy*>bio.get_data(b)
libc.cmsghdr* cmsg = NULL
int res
int is_ktls = bio.test_flags(b, FLAGS_KTLS_RX)
IF DEBUG:
with gil:
print('bio_read_ex(data=%x, datal=%d)' % (<long long>data, datal))
if proxy.flags & FLAGS_PROXY_RECV_SUBMITTED:
if proxy.recv_vec.iov_base != (data + 5 if is_ktls else data):
IF DEBUG:
with gil:
print("bio_read_ex() error: concurrent call")
return 0
if proxy.recv_vec.iov_len > (datal - 21 if is_ktls else datal):
IF DEBUG:
with gil:
print("bio_read_ex() error: short reread")
return 0
bio.clear_retry_flags(b)
if (
proxy.flags & FLAGS_PROXY_RECV_KTLS and
proxy.flags & FLAGS_PROXY_RECV_COMPLETED
):
proxy.flags &= ~FLAGS_PROXY_RECV_ALL
res = proxy.recv_callback.res
if datal < res + 5:
IF DEBUG:
with gil:
print("bio_read_ex() error: datal too short")
errno.errno = errno.EINVAL
return 0
cmsg = libc.CMSG_FIRSTHDR(&proxy.recv_msg)
if cmsg.cmsg_type == linux.TLS_GET_RECORD_TYPE:
data[0] = (<unsigned char *> libc.CMSG_DATA(cmsg))[0]
data[1] = 0x03 # TLS1_2_VERSION_MAJOR
data[2] = 0x03 # TLS1_2_VERSION_MINOR
# returned length is limited to msg_iov.iov_len above
data[3] = (res >> 8) & 0xff
data[4] = res & 0xff
string.memcpy(data + 5, proxy.read_buffer, res)
res += 5
else:
string.memcpy(data, proxy.read_buffer, res)
readbytes[0] = res
IF DEBUG:
with gil:
print("bio_read_ex() read:", res, "(forwarded TLS record)")
print("<<< ", end="")
for i in range(res):
print(
"%02x " % <unsigned char>data[i],
end="" if (i + 1) % 16 or i == res - 1 else "\n<<< ",
)
print()
elif proxy.flags & FLAGS_PROXY_RECV_COMPLETED:
proxy.flags &= ~FLAGS_PROXY_RECV_ALL
res = proxy.recv_callback.res
if res < 0:
IF DEBUG:
with gil:
print("bio_read_ex() error:", -res)
errno.errno = -res
return 0
if is_ktls:
if proxy.recv_msg.msg_controllen:
cmsg = libc.CMSG_FIRSTHDR(&proxy.recv_msg)
if cmsg.cmsg_type == linux.TLS_GET_RECORD_TYPE:
data[0] = (<unsigned char*>libc.CMSG_DATA(cmsg))[0]
data[1] = 0x03 # TLS1_2_VERSION_MAJOR
data[2] = 0x03 # TLS1_2_VERSION_MINOR
# returned length is limited to msg_iov.iov_len above
data[3] = (res >> 8) & 0xff
data[4] = res & 0xff
res += 5
if res == 0:
bio.set_flags(b, bio.FLAGS_IN_EOF)
readbytes[0] = res
IF DEBUG:
with gil:
print(
"bio_read_ex() read:", res, "(TLS record)" if cmsg else ""
)
print("<<< ", end="")
for i in range(res):
print(
"%02x " % <unsigned char>data[i],
end="" if (i + 1) % 16 or i == res - 1 else "\n<<< ",
)
print()
else:
bio.set_retry_read(b)
readbytes[0] = 0
if not proxy.flags & FLAGS_PROXY_RECV_SUBMITTED:
if proxy.flags & FLAGS_PROXY_RECV_KTLS:
reset_msg(&proxy.recv_msg, proxy.cmsg)
IF DEBUG:
with gil:
print("bio_read_ex() submit(%x, %d)" % (
<long long>proxy.recv_vec.iov_base,
proxy.recv_vec.iov_len,
))
elif is_ktls:
if datal < 21:
IF DEBUG:
with gil:
print("bio_read_ex() error: datal too short")
errno.errno = errno.EINVAL
return 0
proxy.recv_vec.iov_base = data + 5
proxy.recv_vec.iov_len = datal - 21
reset_msg(&proxy.recv_msg, proxy.cmsg)
IF DEBUG:
with gil:
print("bio_read_ex() submit(%x, %d)" % (
<long long>proxy.recv_vec.iov_base,
proxy.recv_vec.iov_len,
))
else:
proxy.recv_vec.iov_base = data
proxy.recv_vec.iov_len = datal
reset_msg(&proxy.recv_msg, NULL)
IF DEBUG:
with gil:
print("bio_read_ex() submit")
if not ring_sq_submit_recvmsg(
&proxy.loop.ring.sq,
proxy.fd,
&proxy.recv_msg,
&proxy.recv_callback,
):
IF DEBUG:
with gil:
print("bio_read_ex() error: SQ full")
return 0
proxy.flags |= FLAGS_PROXY_RECV_SUBMITTED
return 1
cdef long bio_ctrl(bio.BIO* b, int cmd, long num, void* ptr) nogil:
cdef:
ssl_h.ktls_crypto_info_t* crypto_info
long ret = 0
if cmd == bio.BIO_CTRL_EOF:
IF DEBUG:
with gil:
print("BIO_CTRL_EOF", ret)
elif cmd == bio.BIO_CTRL_PUSH:
IF DEBUG:
with gil:
print("BIO_CTRL_PUSH", ret)
elif cmd == bio.BIO_CTRL_POP:
IF DEBUG:
with gil:
print("BIO_CTRL_POP", ret)
elif cmd == bio.BIO_CTRL_FLUSH:
ret = 1
IF DEBUG:
with gil:
print('BIO_CTRL_FLUSH', ret)
elif cmd == BIO_CTRL_SET_KTLS:
IF DEBUG:
with gil:
print("BIO_CTRL_SET_KTLS", "TX end" if num else "RX end")
crypto_info = <ssl_h.ktls_crypto_info_t*>ptr
if libc.setsockopt(
(<Proxy*>bio.get_data(b)).fd,
libc.SOL_TLS,
linux.TLS_TX if num else linux.TLS_RX,
crypto_info,
crypto_info.tls_crypto_info_len,
) == 0:
bio.set_flags(b, FLAGS_KTLS_TX if num else FLAGS_KTLS_RX)
else:
IF DEBUG:
with gil:
print(
"BIO_CTRL_SET_KTLS",
"TX end" if num else "RX end",
"failed",
)
elif cmd == BIO_CTRL_GET_KTLS_SEND:
return bio.test_flags(b, FLAGS_KTLS_TX) != 0
elif cmd == BIO_CTRL_GET_KTLS_RECV:
return bio.test_flags(b, FLAGS_KTLS_RX) != 0
else:
IF DEBUG:
with gil:
print('bio_ctrl', cmd, num)
return ret
cdef int bio_create(bio.BIO* b) nogil:
bio.set_init(b, 1)
return 1
cdef int bio_destroy(bio.BIO* b) nogil:
bio.set_shutdown(b, 1)
return 1
cdef int tls_send_cb(RingCallback* cb) nogil except 0:
cdef Proxy* proxy = <Proxy*>cb.data
proxy.flags |= FLAGS_PROXY_SEND_COMPLETED
with gil:
(<TLSTransport>proxy.transport).write_cb(cb.res)
return 1
cdef int tls_recv_cb(RingCallback* cb) nogil except 0:
cdef Proxy* proxy = <Proxy*>cb.data
proxy.flags |= FLAGS_PROXY_RECV_COMPLETED
with gil:
(<TLSTransport>proxy.transport).read_cb(cb.res)
return 1
cdef class TLSTransport:
@staticmethod
def new(
int fd,
protocol,
KLoopImpl loop,
sslctx,
server_side=False,
server_hostname=None,
session=None,
waiter=None,
):
cdef:
TLSTransport rv = TLSTransport.__new__(TLSTransport)
pyssl.PySSLMemoryBIO* c_bio
pyssl.SSL* s
libc.setsockopt(fd, socket.SOL_TCP, linux.TCP_ULP, b"tls", 3)
py_bio = ssl.MemoryBIO()
c_bio = <pyssl.PySSLMemoryBIO*>py_bio
c_bio.bio, rv.bio = rv.bio, c_bio.bio
try:
rv.sslobj = sslctx.wrap_bio(
py_bio, py_bio, server_side, server_hostname, session
)
finally:
c_bio.bio, rv.bio = rv.bio, c_bio.bio
del py_bio
s = (<pyssl.PySSLSocket*>rv.sslobj._sslobj).ssl
ssl_h.set_options(s, ssl_h.OP_ENABLE_KTLS)
ssl_h.clear_mode(
s,
ssl_h.SSL_MODE_RELEASE_BUFFERS |
ssl_h.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER
)
rv.fd = fd
rv.protocol = protocol
rv.loop = loop
rv.sslctx = sslctx
rv.proxy.loop = &loop.loop
rv.proxy.fd = fd
rv.waiter = waiter
rv.write_buffer = collections.deque()
rv.do_handshake()
return rv
def __cinit__(self):
self.state = UNWRAPPED
self.bio = bio.new(KTLS_BIO_METHOD)
self.proxy.transport = <PyObject*>self
self.proxy.send_msg.msg_iov = &self.proxy.send_vec
self.proxy.send_msg.msg_iovlen = 1
self.proxy.send_callback.data = <void*>&self.proxy
self.proxy.send_callback.callback = tls_send_cb
self.proxy.cmsg = PyMem_RawMalloc(CMSG_SIZE)
if self.proxy.cmsg == NULL:
raise MemoryError
self.proxy.recv_msg.msg_iov = &self.proxy.recv_vec
self.proxy.recv_msg.msg_iovlen = 1
self.proxy.recv_callback.data = <void*>&self.proxy
self.proxy.recv_callback.callback = tls_recv_cb
bio.set_data(self.bio, <void*>&self.proxy)
def __dealloc__(self):
self.sslobj = None
bio.free(self.bio)
PyMem_RawFree(self.proxy.read_buffer)
PyMem_RawFree(self.proxy.cmsg)
cdef do_handshake(self):
if self.state == UNWRAPPED:
self.state = HANDSHAKING
elif self.state != HANDSHAKING:
raise RuntimeError("Cannot do handshake now")
try:
IF DEBUG:
print("do_handshake()")
self.sslobj.do_handshake()
except ssl.SSLWantReadError:
IF DEBUG:
print("do_handshake() SSLWantReadError")
except ssl.SSLWantWriteError:
IF DEBUG:
print("do_handshake() SSLWantWriteError")
except Exception as ex:
IF DEBUG:
print('do_handshake() error:', ex)
raise
else:
IF DEBUG:
print('do_handshake() done')
self.state = WRAPPED
if self.waiter:
self.waiter.set_result(self)
self.waiter = None
if bio.test_flags(self.bio, FLAGS_KTLS_RX):
self.proxy.read_buffer = <char*>PyMem_RawMalloc(65536)
if self.proxy.read_buffer == NULL:
raise MemoryError
self.proxy.flags |= FLAGS_PROXY_RECV_KTLS
self.proxy.recv_vec.iov_base = self.proxy.read_buffer
self.proxy.recv_vec.iov_len = 65536
self.do_read_ktls()
else:
self.do_read()
cdef do_read_ktls(self):
cdef:
int res
libc.cmsghdr* cmsg
unsigned char record_type
if self.proxy.flags & FLAGS_PROXY_RECV_COMPLETED:
self.proxy.flags &= ~FLAGS_PROXY_RECV_ALL
res = self.proxy.recv_callback.res
if res < 0:
IF DEBUG:
print("do_read_ktls() error:", -res)
self.loop.call_soon(
self.protocol.connection_lost,
IOError(-res, string.strerror(-res))
)
elif res == 0:
IF DEBUG:
print("do_read_ktls() EOF")
self.loop.call_soon(self.protocol.eof_received)
self.loop.call_soon(self.protocol.connection_lost, None)
else:
if self.proxy.recv_msg.msg_controllen:
cmsg = libc.CMSG_FIRSTHDR(&self.proxy.recv_msg)
if cmsg.cmsg_type == linux.TLS_GET_RECORD_TYPE:
record_type = (<unsigned char*>libc.CMSG_DATA(cmsg))[0]
if record_type != ssl_h.SSL3_RT_APPLICATION_DATA:
IF DEBUG:
print("do_read_ktls() forward CMSG")
self.proxy.flags |= FLAGS_PROXY_RECV_COMPLETED
return self.do_read()
IF DEBUG:
print("do_read_ktls() received", res, "bytes")
self.loop.call_soon(
self.protocol.data_received,
bytes(self.proxy.read_buffer[:res]),
)
self.loop.call_soon(self.do_read_ktls, self)
elif not self.proxy.flags & FLAGS_PROXY_RECV_SUBMITTED:
IF DEBUG:
print("do_read_ktls() submit")
reset_msg(&self.proxy.recv_msg, self.proxy.cmsg)
if not ring_sq_submit_recvmsg(
&self.proxy.loop.ring.sq,
self.fd,
&self.proxy.recv_msg,
&self.proxy.recv_callback,
):
raise RuntimeError("SQ full")
self.proxy.flags |= FLAGS_PROXY_RECV_SUBMITTED
cdef do_read(self):
try:
data = self.sslobj.read(65536)
except ssl.SSLWantReadError:
IF DEBUG:
print("do_read() SSLWantReadError")
except ssl.SSLWantWriteError:
IF DEBUG:
print("do_read() SSLWantWriteError")
except Exception as ex:
IF DEBUG:
print("do_read() error:", ex)
self.loop.call_soon(self.protocol.connection_lost, ex)
else:
if data:
IF DEBUG:
print("do_read() received", len(data), bytes)
print(data)
self.loop.call_soon(self.protocol.data_received, data)
if self.proxy.flags & FLAGS_PROXY_RECV_KTLS:
self.loop.call_soon(self.do_read_ktls, self)
else:
self.loop.call_soon(self.do_read, self)
else:
IF DEBUG:
print("do_read() EOF")
self.loop.call_soon(self.protocol.eof_received)
self.loop.call_soon(self.protocol.connection_lost, None)
cdef write_cb(self, int res):
IF DEBUG:
print("write_cb", res, "state:", self.state)
if self.state == HANDSHAKING:
self.do_handshake()
cdef read_cb(self, int res):
IF DEBUG:
print("read_cb", res, "state:", self.state)
if self.state == HANDSHAKING:
self.do_handshake()
elif self.state == WRAPPED:
if self.proxy.flags & FLAGS_PROXY_RECV_KTLS:
self.do_read_ktls()
else:
self.do_read()
def write(self, data):
if self.sending:
self.write_buffer.append(data)
else:
try:
self.sslobj.write(data)
except ssl.SSLWantWriteError:
IF DEBUG:
print("write() SSLWantWriteError")
cdef bio.Method* KTLS_BIO_METHOD = bio.meth_new(
bio.get_new_index(), "kTLS BIO"
)
if not bio.meth_set_write_ex(KTLS_BIO_METHOD, bio_write_ex):
raise fromOpenSSLError(ImportError)
if not bio.meth_set_read_ex(KTLS_BIO_METHOD, bio_read_ex):
raise fromOpenSSLError(ImportError)
if not bio.meth_set_ctrl(KTLS_BIO_METHOD, bio_ctrl):
raise fromOpenSSLError(ImportError)
if not bio.meth_set_create(KTLS_BIO_METHOD, bio_create):
raise fromOpenSSLError(ImportError)
if not bio.meth_set_destroy(KTLS_BIO_METHOD, bio_destroy):
raise fromOpenSSLError(ImportError)

19
src/kloop/udp.pxd Normal file
View file

@ -0,0 +1,19 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct UDPAction:
int fd
libc.iovec vec
libc.msghdr msg
RingCallback callback
CResolver* resolver
void* rust_waker
libc.sockaddr addr

104
src/kloop/udp.pyx Normal file
View file

@ -0,0 +1,104 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef extern int udp_bind(libc.sockaddr* addr, libc.socklen_t addrlen) nogil:
cdef int fd = libc.socket(addr.sa_family, libc.SOCK_DGRAM, 0)
if fd == -1:
return -1
if libc.bind(fd, addr, addrlen) == -1:
# TODO: close fd
return -1
return fd
cdef extern unsigned long udp_action_init(int fd, CResolver* resolver) nogil:
cdef UDPAction* rv
rv = <UDPAction*>PyMem_RawMalloc(sizeof(UDPAction))
if rv == NULL:
return 0
string.memset(rv, 0, sizeof(UDPAction))
rv.fd = fd
rv.resolver = resolver
rv.msg.msg_iov = &rv.vec
rv.msg.msg_iovlen = 1
rv.callback.data = <void*>rv
rv.callback.callback = udp_action_cb
rv.msg.msg_name = <void*>&rv.addr
rv.msg.msg_namelen = sizeof(rv.addr)
return <unsigned long>rv
cdef int udp_action_cb(RingCallback* cb) nogil except 0:
cdef UDPAction* action = <UDPAction*>cb.data
waker_wake(action.rust_waker)
resolver_run_until_stalled(action.resolver.rust_resolver)
return 1
cdef extern int udp_send_poll(
unsigned long send_in,
const char* data,
size_t datalen,
libc.sockaddr* addr,
libc.socklen_t addrlen,
void* waker,
) nogil:
cdef UDPAction* send = <UDPAction*>send_in
if send.vec.iov_base == NULL:
send.vec.iov_base = data
send.vec.iov_len = datalen
send.addr = addr[0]
send.msg.msg_namelen = addrlen
send.rust_waker = waker
return ring_sq_submit_sendmsg(
&send.resolver.loop.ring.sq,
send.fd,
&send.msg,
&send.callback,
) - 1
else:
waker_forget(waker)
if send.vec.iov_base != data or send.vec.iov_len != datalen:
return -1
return send.callback.res or -1
cdef extern int udp_recv_poll(
unsigned long recv_in,
char* data,
size_t datalen,
void* waker,
) nogil:
cdef UDPAction* recv = <UDPAction*>recv_in
if recv.vec.iov_base == NULL:
recv.vec.iov_base = data
recv.vec.iov_len = datalen
recv.rust_waker = waker
return ring_sq_submit_recvmsg(
&recv.resolver.loop.ring.sq,
recv.fd,
&recv.msg,
&recv.callback,
) - 1
else:
waker_forget(waker)
if recv.vec.iov_base != data or recv.vec.iov_len != datalen:
return -1
return recv.callback.res or -1
cdef extern libc.sockaddr* udp_get_addr(unsigned long recv_in) nogil:
cdef UDPAction* recv = <UDPAction*>recv_in
return &recv.addr
cdef extern void udp_action_free(unsigned long action) nogil:
PyMem_RawFree(<void*>action)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -8,3 +8,67 @@
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef struct SubmissionQueue:
unsigned* khead
unsigned* ktail
unsigned* kring_mask
unsigned* kring_entries
unsigned* kflags
unsigned* kdropped
unsigned* array
linux.io_uring_sqe* sqes
unsigned sqe_head
unsigned sqe_tail
size_t ring_size
void* ring_ptr
cdef struct CompletionQueue:
unsigned* khead
unsigned* ktail
unsigned* kring_mask
unsigned* kring_entries
unsigned* kflags
unsigned* koverflow
linux.io_uring_cqe* cqes
size_t ring_size
void* ring_ptr
cdef struct Ring:
SubmissionQueue sq
CompletionQueue cq
unsigned flags
int ring_fd
unsigned features
int enter_ring_fd
linux.__u8 int_flags
linux.__u8 pad[3]
unsigned pad2
cdef struct RingCallback:
void* data
int res
int (*callback)(RingCallback* cb) nogil except 0
cdef int ring_sq_submit_sendmsg(
SubmissionQueue* sq,
int fd,
const libc.msghdr *msg,
RingCallback* callback,
) nogil
cdef int ring_sq_submit_recvmsg(
SubmissionQueue* sq,
int fd,
const libc.msghdr *msg,
RingCallback* callback,
) nogil

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Fantix King http://fantix.pro
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
@ -8,3 +8,327 @@
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
cdef linux.__u32 SIG_SIZE = libc._NSIG // 8
cdef int ring_init(
Ring* ring,
linux.__u32 queue_depth,
linux.io_uring_params* params
) nogil except 0:
# SYSCALL: SYS_io_uring_setup
ring.ring_fd = ring.enter_ring_fd = libc.syscall(
libc.SYS_io_uring_setup, queue_depth, params
)
if ring.ring_fd < 0:
with gil:
PyErr_SetFromErrno(IOError)
return 0
# mmap the ring_ptr
ring.sq.ring_size = ring.cq.ring_size = max(
params.sq_off.array + params.sq_entries * sizeof(unsigned),
params.cq_off.cqes + params.cq_entries * sizeof(linux.io_uring_cqe)
)
ring.sq.ring_ptr = ring.cq.ring_ptr = mman.mmap(
NULL,
ring.sq.ring_size,
mman.PROT_READ | mman.PROT_WRITE,
mman.MAP_SHARED | mman.MAP_POPULATE,
ring.ring_fd,
linux.IORING_OFF_SQ_RING,
)
if ring.sq.ring_ptr == mman.MAP_FAILED:
with gil:
PyErr_SetFromErrno(IOError)
return 0
# Initialize the SubmissionQueue
ring.sq.khead = <unsigned*>(ring.sq.ring_ptr + params.sq_off.head)
ring.sq.ktail = <unsigned*>(ring.sq.ring_ptr + params.sq_off.tail)
ring.sq.kring_mask = <unsigned*>(ring.sq.ring_ptr + params.sq_off.ring_mask)
ring.sq.kring_entries = <unsigned*>(ring.sq.ring_ptr + params.sq_off.ring_entries)
ring.sq.kflags = <unsigned*>(ring.sq.ring_ptr + params.sq_off.flags)
ring.sq.kdropped = <unsigned*>(ring.sq.ring_ptr + params.sq_off.dropped)
ring.sq.array = <unsigned*>(ring.sq.ring_ptr + params.sq_off.array)
ring.sq.sqes = <linux.io_uring_sqe*>mman.mmap(
NULL,
params.sq_entries * sizeof(linux.io_uring_sqe),
mman.PROT_READ | mman.PROT_WRITE,
mman.MAP_SHARED | mman.MAP_POPULATE,
ring.ring_fd,
linux.IORING_OFF_SQES,
)
if ring.sq.sqes == mman.MAP_FAILED:
mman.munmap(ring.sq.ring_ptr, ring.sq.ring_size)
with gil:
PyErr_SetFromErrno(IOError)
return 0
# Initialize the CompletionQueue
ring.cq.khead = <unsigned*>(ring.cq.ring_ptr + params.cq_off.head)
ring.cq.ktail = <unsigned*>(ring.cq.ring_ptr + params.cq_off.tail)
ring.cq.kring_mask = <unsigned*>(ring.cq.ring_ptr + params.cq_off.ring_mask)
ring.cq.kring_entries = <unsigned*>(ring.cq.ring_ptr + params.cq_off.ring_entries)
ring.cq.koverflow = <unsigned*>(ring.cq.ring_ptr + params.cq_off.overflow)
ring.cq.cqes = <linux.io_uring_cqe*>(ring.cq.ring_ptr + params.cq_off.cqes)
if params.cq_off.flags:
ring.cq.kflags = <unsigned*>(ring.cq.ring_ptr + params.cq_off.flags)
return 1
cdef int ring_uninit(Ring* ring) nogil except 0:
if ring.sq.sqes != NULL:
mman.munmap(
ring.sq.sqes,
ring.sq.kring_entries[0] * sizeof(linux.io_uring_sqe),
)
if ring.sq.ring_ptr != NULL:
mman.munmap(ring.sq.ring_ptr, ring.sq.ring_size)
if ring.ring_fd:
if unistd.close(ring.ring_fd) != 0:
with gil:
PyErr_SetFromErrno(IOError)
return 0
return 1
cdef inline int ring_sq_flush(SubmissionQueue* sq) nogil:
cdef:
unsigned mask = sq.kring_mask[0]
unsigned tail = sq.ktail[0]
unsigned to_submit = sq.sqe_tail - sq.sqe_head
if to_submit:
while to_submit:
sq.array[tail & mask] = sq.sqe_head & mask
tail += 1
sq.sqe_head += 1
to_submit -= 1
atomic.store_explicit(<atomic.uint*>sq.ktail, tail, atomic.release)
return 1
else:
return 0
cdef int ring_select(Ring* ring, long long timeout) nogil except -1:
cdef:
int flags = linux.IORING_ENTER_EXT_ARG
bint need_enter = 0
unsigned ready
unsigned wait_nr = 0
linux.io_uring_getevents_arg arg
linux.__kernel_timespec ts
CompletionQueue* cq = &ring.cq
SubmissionQueue* sq = &ring.sq
string.memset(&arg, 0, sizeof(arg))
# Call enter if we have no CQE ready and timeout is not 0, or else we
# handle the ready CQEs first.
ready = atomic.load_explicit(
<atomic.uint*>cq.ktail, atomic.acquire
) - cq.khead[0]
if not ready and timeout != 0:
flags |= linux.IORING_ENTER_GETEVENTS
if timeout > 0:
ts.tv_sec = timeout // SECOND_NS
ts.tv_nsec = timeout % SECOND_NS
arg.ts = <linux.__u64> &ts
wait_nr = 1
need_enter = 1
# Flush the submission queue, and only wakeup the SQ polling thread if
# there is something for the kernel to handle.
if ring_sq_flush(sq):
atomic.thread_fence(atomic.seq_cst)
if atomic.load_explicit(
<atomic.uint*>sq.kflags, atomic.relaxed
) & linux.IORING_SQ_NEED_WAKEUP:
arg.ts = 0
flags |= linux.IORING_ENTER_SQ_WAKEUP
need_enter = 1
if need_enter:
arg.sigmask = 0
arg.sigmask_sz = SIG_SIZE
if libc.syscall(
libc.SYS_io_uring_enter,
ring.enter_ring_fd,
0,
wait_nr,
flags,
&arg,
sizeof(arg),
) < 0:
if errno.errno != errno.ETIME:
with gil:
PyErr_SetFromErrno(IOError)
return -1
ready = atomic.load_explicit(
<atomic.uint*>cq.ktail, atomic.acquire
) - cq.khead[0]
return ready
cdef inline void ring_cq_pop(CompletionQueue* cq, RingCallback** callback) nogil:
cdef:
unsigned head
linux.io_uring_cqe* cqe
RingCallback* ret
head = cq.khead[0]
cqe = cq.cqes + (head & cq.kring_mask[0])
ret = <RingCallback*>cqe.user_data
if ret != NULL:
ret.res = cqe.res
callback[0] = ret
atomic.store_explicit(<atomic.uint*>cq.khead, head + 1, atomic.release)
cdef inline linux.io_uring_sqe* ring_sq_submit(
SubmissionQueue* sq,
linux.__u8 op,
int fd,
unsigned long addr,
unsigned len,
linux.__u64 offset,
bint link,
RingCallback* callback,
) nogil:
cdef:
unsigned int head, next
linux.io_uring_sqe* sqe
head = atomic.load_explicit(<atomic.uint*>sq.khead, atomic.acquire)
next = sq.sqe_tail + 1
if next - head <= sq.kring_entries[0]:
sqe = &sq.sqes[sq.sqe_tail & sq.kring_mask[0]]
sq.sqe_tail = next
string.memset(sqe, 0, sizeof(linux.io_uring_sqe))
sqe.opcode = op
sqe.fd = fd
sqe.off = offset
sqe.addr = addr
sqe.len = len
if link:
sqe.flags = linux.IOSQE_IO_LINK
sqe.user_data = <linux.__u64>callback
return sqe
else:
return NULL
cdef int ring_sq_submit_connect(
SubmissionQueue* sq, int fd, libc.sockaddr* addr, RingCallback* callback
) nogil:
return 1 if ring_sq_submit(
sq,
linux.IORING_OP_CONNECT,
fd,
<unsigned long>addr,
0,
sizeof(addr[0]),
0,
callback,
) else 0
cdef int ring_sq_submit_openat(
SubmissionQueue* sq,
int dfd,
const char* path,
int flags,
mode_t mode,
RingCallback* callback,
) nogil:
cdef linux.io_uring_sqe* sqe = ring_sq_submit(
sq,
linux.IORING_OP_OPENAT,
dfd,
<unsigned long>path,
mode,
0,
0,
callback,
)
if sqe == NULL:
return 0
else:
sqe.open_flags = flags
return 1
cdef int ring_sq_submit_read(
SubmissionQueue* sq,
int fd,
char* buf,
unsigned nbytes,
linux.__u64 offset,
RingCallback* callback,
) nogil:
return 1 if ring_sq_submit(
sq,
linux.IORING_OP_READ,
fd,
<unsigned long>buf,
nbytes,
offset,
0,
callback,
) else 0
cdef int ring_sq_submit_close(
SubmissionQueue* sq,
int fd,
RingCallback * callback,
) nogil:
return 1 if ring_sq_submit(
sq,
linux.IORING_OP_CLOSE,
fd,
0,
0,
0,
0,
callback,
) else 0
cdef int ring_sq_submit_sendmsg(
SubmissionQueue* sq,
int fd,
const libc.msghdr *msg,
RingCallback* callback,
) nogil:
return 1 if ring_sq_submit(
sq,
linux.IORING_OP_SENDMSG,
fd,
<unsigned long>msg,
1,
0,
0,
callback,
) else 0
cdef int ring_sq_submit_recvmsg(
SubmissionQueue* sq,
int fd,
const libc.msghdr *msg,
RingCallback* callback,
) nogil:
return 1 if ring_sq_submit(
sq,
linux.IORING_OP_RECVMSG,
fd,
<unsigned long>msg,
1,
0,
0,
callback,
) else 0

28
tests/cert.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIErjCCApYCCQDzWJgnuLzufzANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5s
b2NhbGhvc3Q6ODA4ODAeFw0yMjA3MDIyMDQ5MzZaFw0yMzA3MDIyMDQ5MzZaMBkx
FzAVBgNVBAMMDmxvY2FsaG9zdDo4MDg4MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
MIICCgKCAgEAsTtnZV5hayOTVfyq6MbkLRaJj4tVoamMXmqTepqpEwQFxUY80MUi
iTcnoxeKTHyToEcPDBINxDAQyvYZQyReo2CScPEBeYlW8Ur6DcZ6CslcrQKIAQEp
JQ6a/DwZT/GS71qQfaOkiO+tKU7TbaPvF3a6kK+JFOgBaFbkfcb6Ef0xUfhrgfut
fQFDrF4up3otlFY3thofTgoLcpx04OHIeveMWHEemEi8LOr/LRV5MDAwlWSqSHuF
JMn/U9fPXZXiv450QINtrBY43n9xAP1sQZL4YowYbkbxWjj/ghpzwqqWZz/sxzHr
ayy6UJcOkYiY+RzkLPZ5GLU1BJlWUKNueUtZZQ+n3OOYUGFpbeGnOl6YB1qqvNtn
8Lvsmqu0nc3gDsINnxi4E3KEstAJlk7X1M8l7XCgjXhTXrv2ebvQyd7gEAMIXtuy
Rpv75Kga+ydapxLnI1spF0q31eYuA0EJlWBcLd0nnU/e0GuhmqWB7PCFm2JQ3LyY
1GFUuvR0/VAoDf06vyicpTIkuCMrSsbSUqNZamyKyfujUgLqbUlfBfkV++q1cFGY
MQJBTI6U9qNc4JBxuiHbPENMVLQQAUsSEbdltsaOYV9rMhZn5FdZa/plOuC7Q5+C
7SllnnRKWJIl7pQh3bF6NNq3cjtuu3suDipFoFcyo5f06wpxJHbiZH0CAwEAATAN
BgkqhkiG9w0BAQsFAAOCAgEAcc7L+4z7B9SESGKeVVZavt6pvApM9JIJ7SsRjuDp
I026Ld7LkOp+QyQ30yVhmYCxKYFCaJSngHsusk8/TMr5OU80ixGEzUXzaq82UJkQ
zkfMHm7k3/UKkJ4I4pT7lLeG20M+QQVB5a8tTFBGDt3Rkc54nuxKNq5JKwQ0AaHz
aid8LhHZwQMOiRMJgaga0bjrKo8I9yTcOvip+4UFitJe0EiSjLKWS4eyrWN7I7t9
T1OctNWb8aILnO9SvjREvVFk5rN70F0nOM0PSQGUdO0KfNAuE+2e47avlTHwH+I9
0m1PEDcLikcoo0rCC+8tLDvwuBS+GdH+NKQYSdlnik9EU7dSo+cRHDzTS9oU8/Kv
81teIY45pUN0AzoySxgjBr8Z/lKiEXhUq/OCZHEl6lOZGtExuwlwr8ZSZNqNFZkh
iqNb8p81ER8nbk6U2T6tx7heBFb0hipfp5hywLL7DMffx1iqEdsIEqYJiFPT4oen
34zM0vft/zMys8oqvo82sNSeQUwFfq0Tr5IopJSaGjud+x1qqRW8XEsyW5t5YEsk
BjEcRgXWHsXOKRGuTdosftJyf6S/3MCysLrkwftOxWNi8Hin55XxzS6nmoGy0Y4y
SOvyGjrlkzRdb187E5Exm2gqArTDJgvl0wFegRxqmQnAe0ABdLz/BYm9nt4AUQca
ZeY=
-----END CERTIFICATE-----

54
tests/debug.py Normal file
View file

@ -0,0 +1,54 @@
import asyncio
import ssl
import socket
import threading
import time
import pathlib
import kloop
def server():
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
cur = pathlib.Path(__file__).parent
ctx.load_cert_chain(cur / "cert.pem", cur / "key.pem")
ss = socket.socket()
ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ss.bind(("localhost", 8088))
ss.listen(1)
s, addr = ss.accept()
s = ctx.wrap_socket(s, server_side=True)
print(s)
while True:
req = s.recv(65536)
print("<<<", req)
if req == b'Hello':
s.send(b'world\n')
elif req.startswith(b'Sleep'):
time.sleep(float(req.split()[1]))
s.send(b'Sleep done\n')
elif req == b'Bye':
s.send(b'So long!\n')
s.close()
break
else:
s.send(b'unknown command\n')
async def main():
ctx = ssl.create_default_context(cafile="tests/cert.pem")
r, w = await asyncio.open_connection("localhost", 8088, ssl=ctx)
print(r, w)
w.write(b'Sleep 3')
print(await r.readline())
w.write(b'Hello')
print(await r.readline())
t = threading.Thread(target=server)
t.daemon = True
t.start()
asyncio.set_event_loop_policy(kloop.KLoopPolicy())
asyncio.run(main())

52
tests/key.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCxO2dlXmFrI5NV
/KroxuQtFomPi1WhqYxeapN6mqkTBAXFRjzQxSKJNyejF4pMfJOgRw8MEg3EMBDK
9hlDJF6jYJJw8QF5iVbxSvoNxnoKyVytAogBASklDpr8PBlP8ZLvWpB9o6SI760p
TtNto+8XdrqQr4kU6AFoVuR9xvoR/TFR+GuB+619AUOsXi6nei2UVje2Gh9OCgty
nHTg4ch694xYcR6YSLws6v8tFXkwMDCVZKpIe4Ukyf9T189dleK/jnRAg22sFjje
f3EA/WxBkvhijBhuRvFaOP+CGnPCqpZnP+zHMetrLLpQlw6RiJj5HOQs9nkYtTUE
mVZQo255S1llD6fc45hQYWlt4ac6XpgHWqq822fwu+yaq7SdzeAOwg2fGLgTcoSy
0AmWTtfUzyXtcKCNeFNeu/Z5u9DJ3uAQAwhe27JGm/vkqBr7J1qnEucjWykXSrfV
5i4DQQmVYFwt3SedT97Qa6GapYHs8IWbYlDcvJjUYVS69HT9UCgN/Tq/KJylMiS4
IytKxtJSo1lqbIrJ+6NSAuptSV8F+RX76rVwUZgxAkFMjpT2o1zgkHG6Ids8Q0xU
tBABSxIRt2W2xo5hX2syFmfkV1lr+mU64LtDn4LtKWWedEpYkiXulCHdsXo02rdy
O267ey4OKkWgVzKjl/TrCnEkduJkfQIDAQABAoICAQCTIsYMGfa2g6pl0IXzCmuU
IwnA6eQFekTWfZRCTPPgnY5M4KrMh1zMncWCWKCwLxxAC74qPzK+lUjbtsPyQddv
u1TX/r1CsQpE+AHwPb5EBjeySk+uxc/qu91fWmWJQPzDSWR7acfHB3Oyv4Y3l8l+
qUrpo1Ei0hmZDcpTwUUKejDf5GUcXF0DUnFkQCxrTbtZUtqdi8JBf4VReSJvMALI
U4toQUEtG/VdNRrzysf5gYhT/maAFkvK5wfaC2hUQYMllJZemL4MB8besN6X+8xM
BD/BUJ1Xr/3e8ULuOBeynPyQazmfhYn0SAXFsiKeH9EfvySPT2/pW0bQspw+BfBH
0IyZXaH0tqvvTLfhfUBB5CtW8KnJ7n2r9QZbX8+Y8WnyD/BmWhAjN/wihph12vNO
wxPlA95yKX3KTNDwTCjvtmgNQU+IkujTfBsH9v701ASQcZhScYLQZ6kksiFTagbf
Sk9Pk3i2Et465wBMOrPi2GmcPEXLz4IKK0k5ex3WYi3mIBuCZx/vFOOo/K6x637k
g5NeunFJg1n3w/igVOCK2sQQWU9bQHwwVlj0YX4ZrUeC+/l3V5xTZVrqn8TWiHfx
8N3DF5oEkW/jbEuI1rrNo1X8Ma+Ij2E1rDPYcNh/4/STIg/i4FRM0qYf13cpzgE0
XwBrR8BdWhgSZODGuen44QKCAQEA3m8TtziLbxD0Uc/Xe8ax7eHmi1Bvn4G95dgM
TI5ZKOVFEIObe2036cwvTvIsJbDi+AbjXpswQdyxwGku9nZRv8jgiMyNGJKMiyPc
RQKFkxfvL+SpQnjITFvPiXdXBVUWWKhBjsNXn29iG9a43WI0yJc8DVEzeOMncwvP
gErHu8wXSweBmCSwWD8EE7+CLHJyEB7klF314hkYImSDepKAhXZEsQ0u2BYOsmtD
Gie4F8U5kDHJgTV1riMEvLYJFjcYxvhcgTaZOCP4GgVUrnZ8zk1PFQjA2Rc3np5b
HCL3Q5aCU/V/tDFKXSCY7GoMbqid4GCsF/wQes0/KDgHpDiutwKCAQEAy/oeQWHx
xHnuXtbWtAM+E9M/gbHhnZSxcxwexe87kW4a6kDY15GPpe8BvbfIxoafEv8UzKGf
kzZnXwXyXVgUX99rz3gcu40ohT0LIBlE+E4j7TT5DXwD9l9Hd3LHbqkykxiRUEW8
2G2PL45qGu9JSA5Yh2QgQxRy8BNe/ilzDE14YWnr+5XaLaMDOAAFwpwHQkzEpAqh
o2eLJ6BHn0qL328uX30FlnURjKig4Ag8uPuDmf6UIyIqs4IlfnML+x7UKNprTBgl
5vctgcD6xt6fYYcA4GHyibJWqPWw2+6wQgEuCSCP5qqIqksj2ageB4nU3acF5mZR
V7Mcta2iP9uSawKCAQACbvqrPX4hB/F0V67G1uSpcphAG/AssZGvSw7PY5tMHD4G
MTppPkenUimOVo3vF6FUD1c7eL0ta0myjjQKVD8OtxblyD8e7rOE7i6BvsZRVqiy
QHEtnf6q/zIlEd4s28Jz09trwW1a/C/5l/7LxUBIlYb9qk7C2tFaq91oTOkkf9Yd
Zwae8H/RZ1cXFDhLXjErRftAOErxX07pSWo61BF95E5aFYfWq18bypqgVaDo+apo
jnLi6//OmOl7Ww58ZLvrqVnxu/QvLg2P01Ea24cABs5/r2nUtjFQlfDvPN6xqZrh
akUsZoGmxS2HIJ3kNAoOI2Ceno5bmZVATmSfAlO/AoIBACtKUSOFOv989Ucxh227
BnDEs8S90OlVxndsr2dIx7aszI+M5biSjw8jc5YlLDpeFeK9OlfXsXtX34Z+R5rh
96kFTOdCUa7IXaIxe2v7kPS1+M/+HyFqgZjHTe6e8I6e4Dnxy91if5rbXbk1G3XN
9RqS2N0bNfGmpGIpak6800r4IiViio/hlOV8pFE9R/uui6fZjR5Xl0iL0C+/x5Oa
CaeI/CmN6iKtg+T7YPFLLkAAOUT0j9IJDVD9vSFiH1fTiimFeaIzuc+UBBd2soKE
ewUC1v/IKeZpcBT51+hFdyj7AR80xvWomcqymdA26vCkowXLzefBLWBUhjJUwIuW
+TMCggEBAIe10HbMp0nbogdAGE28gEpjzkBBeEl/EgMhN8EjemrLmjWI4rK3+Nyh
Jo6W4QG4dT9C7KUP9RzDOoF4kkaFaPZ/KqlSn2qUVYLf9N4s95qQvGlVFIjy5tEp
ChYdiuP71DNXCjolQIq7nb1zkgbTbvuwVSnw2QwQnYdkZMulgjVrvsK55jrOnsuO
EMxjVrlNxeUrb2zLaBsfA5uTPgKgtDGmTDMx+wFAay5jKc9Zmgr1+z834bCKStAh
7gcRoVAjkiu2hoNxBTXgqtFtCizyajiiUUYfFAswJ9RLv7qdrqnpuCv1cffrALmd
PcTNbfrOMsRvyHwyfUIHDaBT62YTztQ=
-----END PRIVATE KEY-----

View file

@ -1,9 +1,58 @@
# Copyright (c) 2022 Fantix King https://fantix.pro
# kLoop is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
import asyncio
import ssl
import time
import unittest
from kloop import uring, ktls
import kloop
class TestLoop(unittest.TestCase):
def test_loop(self):
self.assertIsNotNone(uring)
self.assertIsNotNone(ktls)
def setUp(self):
asyncio.set_event_loop_policy(kloop.KLoopPolicy())
self.loop = asyncio.new_event_loop()
def tearDown(self):
self.loop.close()
asyncio.set_event_loop_policy(None)
def test_call_soon(self):
self.loop.call_soon(self.loop.stop)
self.loop.run_forever()
def test_call_later(self):
secs = 0.1
self.loop.call_later(secs, self.loop.stop)
start = time.monotonic()
self.loop.run_forever()
self.assertGreaterEqual(time.monotonic() - start, secs)
def test_connect(self):
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
# ctx.check_hostname = False
# ctx.verify_mode = ssl.CERT_NONE
# ctx.minimum_version = ssl.TLSVersion.TLSv1_3
host = "www.google.com"
r, w = self.loop.run_until_complete(
# asyncio.open_connection("127.0.0.1", 8080, ssl=ctx)
asyncio.open_connection(host, 443, ssl=ctx)
)
self.loop.run_until_complete(asyncio.sleep(1))
print('send request')
w.write(b"GET / HTTP/1.1\r\n" +
f"Host: {host}\r\n".encode("ISO-8859-1") +
b"Connection: close\r\n"
b"\r\n")
while line := self.loop.run_until_complete(r.read()):
print(line)
w.close()
self.loop.run_until_complete(w.wait_closed())